Merge "Don't automatically remove autogroup summaries"
diff --git a/api/current.txt b/api/current.txt
index f27cf03..5107375 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -32924,6 +32924,7 @@
field public static final java.lang.String ACTION_WIFI_IP_SETTINGS = "android.settings.WIFI_IP_SETTINGS";
field public static final java.lang.String ACTION_WIFI_SETTINGS = "android.settings.WIFI_SETTINGS";
field public static final java.lang.String ACTION_WIRELESS_SETTINGS = "android.settings.WIRELESS_SETTINGS";
+ field public static final java.lang.String ACTION_ZEN_MODE_PRIORITY_SETTINGS = "android.settings.ZEN_MODE_PRIORITY_SETTINGS";
field public static final java.lang.String AUTHORITY = "settings";
field public static final java.lang.String EXTRA_ACCOUNT_TYPES = "account_types";
field public static final java.lang.String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled";
diff --git a/api/system-current.txt b/api/system-current.txt
index df238f5..2f73e92 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -26131,9 +26131,11 @@
}
public final class RecommendationResult implements android.os.Parcelable {
- ctor public RecommendationResult(android.net.wifi.WifiConfiguration);
+ method public static android.net.RecommendationResult createDoNotConnectRecommendation();
+ method public static android.net.RecommendationResult createConnectRecommendation(android.net.wifi.WifiConfiguration);
method public int describeContents();
method public android.net.wifi.WifiConfiguration getWifiConfiguration();
+ method public boolean hasRecommendation();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.net.RecommendationResult> CREATOR;
}
@@ -27183,6 +27185,7 @@
field public int level;
field public java.lang.CharSequence operatorFriendlyName;
field public long timestamp;
+ field public boolean untrusted;
field public java.lang.CharSequence venueName;
}
@@ -35729,6 +35732,7 @@
field public static final java.lang.String ACTION_WIFI_SAVED_NETWORK_SETTINGS = "android.settings.WIFI_SAVED_NETWORK_SETTINGS";
field public static final java.lang.String ACTION_WIFI_SETTINGS = "android.settings.WIFI_SETTINGS";
field public static final java.lang.String ACTION_WIRELESS_SETTINGS = "android.settings.WIRELESS_SETTINGS";
+ field public static final java.lang.String ACTION_ZEN_MODE_PRIORITY_SETTINGS = "android.settings.ZEN_MODE_PRIORITY_SETTINGS";
field public static final java.lang.String AUTHORITY = "settings";
field public static final java.lang.String EXTRA_ACCOUNT_TYPES = "account_types";
field public static final java.lang.String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled";
@@ -35755,6 +35759,8 @@
method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+ method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String, java.lang.String, boolean);
+ method public static void resetToDefaults(android.content.ContentResolver, java.lang.String);
field public static final java.lang.String ADB_ENABLED = "adb_enabled";
field public static final java.lang.String AIRPLANE_MODE_ON = "airplane_mode_on";
field public static final java.lang.String AIRPLANE_MODE_RADIOS = "airplane_mode_radios";
@@ -35828,6 +35834,8 @@
method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
+ method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String, java.lang.String, boolean);
+ method public static void resetToDefaults(android.content.ContentResolver, java.lang.String);
method public static final deprecated void setLocationProviderEnabled(android.content.ContentResolver, java.lang.String, boolean);
field public static final java.lang.String ACCESSIBILITY_DISPLAY_INVERSION_ENABLED = "accessibility_display_inversion_enabled";
field public static final java.lang.String ACCESSIBILITY_ENABLED = "accessibility_enabled";
diff --git a/api/test-current.txt b/api/test-current.txt
index 4f1f147..4c08084 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -33038,6 +33038,7 @@
field public static final java.lang.String ACTION_WIFI_IP_SETTINGS = "android.settings.WIFI_IP_SETTINGS";
field public static final java.lang.String ACTION_WIFI_SETTINGS = "android.settings.WIFI_SETTINGS";
field public static final java.lang.String ACTION_WIRELESS_SETTINGS = "android.settings.WIRELESS_SETTINGS";
+ field public static final java.lang.String ACTION_ZEN_MODE_PRIORITY_SETTINGS = "android.settings.ZEN_MODE_PRIORITY_SETTINGS";
field public static final java.lang.String AUTHORITY = "settings";
field public static final java.lang.String EXTRA_ACCOUNT_TYPES = "account_types";
field public static final java.lang.String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled";
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index 1ad43fa..8185818 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.GET_ACCOUNTS;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.Size;
@@ -30,6 +31,8 @@
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.res.Resources;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.database.SQLException;
import android.os.Build;
import android.os.Bundle;
@@ -46,6 +49,9 @@
import com.google.android.collect.Maps;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.SuppressWarnings;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -150,6 +156,7 @@
* {@link IllegalStateException} if they are used on the main thread.
*/
public class AccountManager {
+
private static final String TAG = "AccountManager";
public static final int ERROR_CODE_REMOTE_EXCEPTION = 1;
@@ -270,6 +277,54 @@
"android.accounts.AccountAuthenticator";
public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({VISIBILITY_UNDEFINED, VISIBILITY_VISIBLE, VISIBILITY_USER_MANAGED_VISIBLE,
+ VISIBILITY_NOT_VISIBLE, VISIBILITY_USER_MANAGED_NOT_VISIBLE})
+ public @interface AccountVisibility {
+ }
+
+ /**
+ * Account visibility was not set.
+ * @hide
+ */
+ public static final int VISIBILITY_UNDEFINED = 0;
+
+ /**
+ * Account is always visible to given application and only authenticator can revoke visibility.
+ * @hide
+ */
+ public static final int VISIBILITY_VISIBLE = 1;
+
+ /**
+ * Account is visible to given application, but user can revoke visibility.
+ * @hide
+ */
+ public static final int VISIBILITY_USER_MANAGED_VISIBLE = 2;
+
+ /**
+ * Account is not visible to given application and only authenticator can grant visibility.
+ * @hide
+ */
+ public static final int VISIBILITY_NOT_VISIBLE = 3;
+
+ /**
+ * Account is not visible to given application, but user can reveal it, for example, using
+ * {@link #newChooseAccountIntent(Account, List, String[], String, String, String[], Bundle)}
+ * @hide
+ */
+ public static final int VISIBILITY_USER_MANAGED_NOT_VISIBLE = 4;
+
+ /**
+ * Key to manifest entry with a list of account types in which application is interested.
+ * Example value: "com.google;com.customtype". If it is specified then the application
+ * will only get notifications related to the types in the list (see
+ * {@link #ACTION_VISIBLE_ACCOUNTS_CHANGED}). Authenticators managing whitelisted types will be
+ * able to know about the application using {@link #ACTION_ACCOUNTS_LISTENER_PACKAGE_INSTALLED}
+ * @hide
+ */
+ public static final String SUPPORTED_ACCOUNT_TYPES = "android.accounts.SupportedAccountTypes";
+
/**
* Token type for the special case where a UID has access only to an account
* but no authenticator specific auth token types.
@@ -284,16 +339,55 @@
private final Handler mMainHandler;
/**
- * Action sent as a broadcast Intent by the AccountsService
- * when accounts are added, accounts are removed, or an
- * account's credentials (saved password, etc) are changed.
+ * Action sent as a broadcast Intent by the AccountsService when accounts are added, accounts
+ * are removed, or an account's credentials (saved password, etc) are changed.
*
* @see #addOnAccountsUpdatedListener
+ *
+ * Deprecated - use ACTION_VISIBLE_ACCOUNTS_CHANGED instead.
*/
public static final String LOGIN_ACCOUNTS_CHANGED_ACTION =
"android.accounts.LOGIN_ACCOUNTS_CHANGED";
/**
+ * Action sent as a broadcast Intent by the AccountsService when accounts potentially visible to
+ * the applications are added, accounts are removed, or an account's credentials (saved
+ * password, etc) are changed. List of supported account types shoud be specified in the
+ * Manifest file using {@link #SUPPORTED_ACCOUNT_TYPES}
+ *
+ * @see #addOnAccountsUpdatedListener
+ * @hide
+ */
+ public static final String ACTION_VISIBLE_ACCOUNTS_CHANGED =
+ "android.accounts.action.VISIBLE_ACCOUNTS_CHANGED";
+
+ /**
+ * Authenticators may subscribe to get notifications about apps interested in their managed account
+ * types using {@link #SUPPORTED_ACCOUNT_TYPES}.
+ * @hide
+ */
+ public static final String ACTION_ACCOUNTS_LISTENER_PACKAGE_INSTALLED =
+ "android.accounts.action.ACCOUNTS_LISTENER_PACKAGE_INSTALLED";
+
+ /**
+ * Uid key to set default visibility for applications targeting API level
+ * {@link android.os.Build.VERSION_CODES#O} or above. See {@link #getAccountVisibility}. If the
+ * value was not set by authenticator USER_MANAGED_NOT_VISIBLE is used.
+ * @hide
+ */
+ public static final int DEFAULT_VISIBILITY = -2;
+
+ /**
+ * Uid key to set visibility for applications targeting API level below
+ * {@link android.os.Build.VERSION_CODES#O}, which were able to see the account before. It
+ * includes applications with GET_ACCOUNTS permission or with the same signature as
+ * authenticator. See {@link #getAccountVisibility}. If the value was not set by authenticator
+ * USER_MANAGED_VISIBLE is used.
+ * @hide
+ */
+ public static final int DEFAULT_LEGACY_VISIBILITY = -3;
+
+ /**
* @hide
*/
public AccountManager(Context context, IAccountManager service) {
@@ -346,18 +440,21 @@
}
/**
- * Gets the saved password associated with the account.
- * This is intended for authenticators and related code; applications
- * should get an auth token instead.
+ * Gets the saved password associated with the account. This is intended for authenticators and
+ * related code; applications should get an auth token instead.
*
- * <p>It is safe to call this method from the main thread.
+ * <p>
+ * It is safe to call this method from the main thread.
*
- * <p>This method requires the caller to have a signature match with the
- * authenticator that owns the specified account.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
*
- * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
- * AUTHENTICATE_ACCOUNTS permission is needed for those platforms. See docs for
- * this function in API level 22.
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before, AUTHENTICATE_ACCOUNTS
+ * permission is needed for those platforms. See docs for this function in API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
*
* @param account The account to query for a password. Must not be {@code null}.
* @return The account's password, null if none or if the account doesn't exist
@@ -372,19 +469,22 @@
}
/**
- * Gets the user data named by "key" associated with the account.
- * This is intended for authenticators and related code to store
- * arbitrary metadata along with accounts. The meaning of the keys
- * and values is up to the authenticator for the account.
+ * Gets the user data named by "key" associated with the account. This is intended for
+ * authenticators and related code to store arbitrary metadata along with accounts. The meaning
+ * of the keys and values is up to the authenticator for the account.
*
- * <p>It is safe to call this method from the main thread.
+ * <p>
+ * It is safe to call this method from the main thread.
*
- * <p>This method requires the caller to have a signature match with the
- * authenticator that owns the specified account.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
*
- * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
- * AUTHENTICATE_ACCOUNTS permission is needed for those platforms. See docs
- * for this function in API level 22.
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before, AUTHENTICATE_ACCOUNTS
+ * permission is needed for those platforms. See docs for this function in API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
*
* @param account The account to query for user data
* @return The user data, null if the account or key doesn't exist
@@ -440,21 +540,18 @@
}
/**
- * Lists all accounts of any type registered on the device.
- * Equivalent to getAccountsByType(null).
+ * Lists all accounts visible to the caller regardless of type. Equivalent to
+ * getAccountsByType(null). These accounts may be visible because the user granted access to the
+ * account, or the AbstractAcccountAuthenticator managing the account did so or because the
+ * client shares a signature with the managing AbstractAccountAuthenticator.
*
- * <p>It is safe to call this method from the main thread.
+ * <p>
+ * It is safe to call this method from the main thread.
*
- * <p>Clients of this method that have not been granted the
- * {@link android.Manifest.permission#GET_ACCOUNTS} permission,
- * will only see those accounts managed by AbstractAccountAuthenticators whose
- * signature matches the client.
- *
- * @return An array of {@link Account}, one for each account. Empty
- * (never null) if no accounts have been added.
+ * @return An array of {@link Account}, one for each account. Empty (never null) if no accounts
+ * have been added.
*/
@NonNull
- @RequiresPermission(GET_ACCOUNTS)
public Account[] getAccounts() {
try {
return mService.getAccounts(null, mContext.getOpPackageName());
@@ -465,21 +562,16 @@
/**
* @hide
- * Lists all accounts of any type registered on the device for a given
- * user id. Equivalent to getAccountsByType(null).
+ * Lists all accounts visible to caller regardless of type for a given user id. Equivalent to
+ * getAccountsByType(null).
*
- * <p>It is safe to call this method from the main thread.
+ * <p>
+ * It is safe to call this method from the main thread.
*
- * <p>Clients of this method that have not been granted the
- * {@link android.Manifest.permission#GET_ACCOUNTS} permission,
- * will only see those accounts managed by AbstractAccountAuthenticators whose
- * signature matches the client.
- *
- * @return An array of {@link Account}, one for each account. Empty
- * (never null) if no accounts have been added.
+ * @return An array of {@link Account}, one for each account. Empty (never null) if no accounts
+ * have been added.
*/
@NonNull
- @RequiresPermission(GET_ACCOUNTS)
public Account[] getAccountsAsUser(int userId) {
try {
return mService.getAccountsAsUser(null, userId, mContext.getOpPackageName());
@@ -524,29 +616,41 @@
}
/**
- * Lists all accounts of a particular type. The account type is a
- * string token corresponding to the authenticator and useful domain
- * of the account. For example, there are types corresponding to Google
- * and Facebook. The exact string token to use will be published somewhere
- * associated with the authenticator in question.
+ * Lists all accounts of particular type visible to the caller. These accounts may be visible
+ * because the user granted access to the account, or the AbstractAcccountAuthenticator managing
+ * the account did so or because the client shares a signature with the managing
+ * AbstractAccountAuthenticator.
*
- * <p>It is safe to call this method from the main thread.
+ * <p>
+ * The account type is a string token corresponding to the authenticator and useful domain of
+ * the account. For example, there are types corresponding to Google and Facebook. The exact
+ * string token to use will be published somewhere associated with the authenticator in
+ * question.
*
- * <p>Clients of this method that have not been granted the
- * {@link android.Manifest.permission#GET_ACCOUNTS} permission,
- * will only see those accounts managed by AbstractAccountAuthenticators whose
- * signature matches the client.
+ * <p>
+ * It is safe to call this method from the main thread.
*
- * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
- * GET_ACCOUNTS permission is needed for those platforms, irrespective of uid
- * or signature match. See docs for this function in API level 22.
+ * <p>
+ * Caller targeting API level {@link android.os.Build.VERSION_CODES#O} and above, will get list
+ * of accounts made visible to it by user or AbstractAcccountAuthenticator and
+ * {@link android.Manifest.permission#GET_ACCOUNTS} permission is not used.
+ *
+ * <p>
+ * Caller targeting API level below {@link android.os.Build.VERSION_CODES#O} that have not been
+ * granted the {@link android.Manifest.permission#GET_ACCOUNTS} permission, will only see those
+ * accounts managed by AbstractAccountAuthenticators whose signature matches the client.
+ *
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before, GET_ACCOUNTS permission is
+ * needed for those platforms, irrespective of uid or signature match. See docs for this
+ * function in API level {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
*
* @param type The type of accounts to return, null to retrieve all accounts
- * @return An array of {@link Account}, one per matching account. Empty
- * (never null) if no accounts of the specified type have been added.
+ * @return An array of {@link Account}, one per matching account. Empty (never null) if no
+ * accounts of the specified type have been added.
*/
@NonNull
- @RequiresPermission(GET_ACCOUNTS)
public Account[] getAccountsByType(String type) {
return getAccountsByTypeAsUser(type, Process.myUserHandle());
}
@@ -612,30 +716,28 @@
}
/**
- * Finds out whether a particular account has all the specified features.
- * Account features are authenticator-specific string tokens identifying
- * boolean account properties. For example, features are used to tell
- * whether Google accounts have a particular service (such as Google
- * Calendar or Google Talk) enabled. The feature names and their meanings
- * are published somewhere associated with the authenticator in question.
+ * Finds out whether a particular account has all the specified features. Account features are
+ * authenticator-specific string tokens identifying boolean account properties. For example,
+ * features are used to tell whether Google accounts have a particular service (such as Google
+ * Calendar or Google Talk) enabled. The feature names and their meanings are published
+ * somewhere associated with the authenticator in question.
*
- * <p>This method may be called from any thread, but the returned
- * {@link AccountManagerFuture} must not be used on the main thread.
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
*
- * <p>This method requires the caller to hold the permission
- * {@link android.Manifest.permission#GET_ACCOUNTS} or be a signature
- * match with the AbstractAccountAuthenticator that manages the account.
+ * <p>
+ * If caller target API level is below {@link android.os.Build.VERSION_CODES#O}, it is
+ * required to hold the permission {@link android.Manifest.permission#GET_ACCOUNTS} or have a
+ * signature match with the AbstractAccountAuthenticator that manages the account.
*
* @param account The {@link Account} to test
* @param features An array of the account features to check
- * @param callback Callback to invoke when the request completes,
- * null for no callback
- * @param handler {@link Handler} identifying the callback thread,
- * null for the main thread
- * @return An {@link AccountManagerFuture} which resolves to a Boolean,
- * true if the account exists and has all of the specified features.
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Boolean, true if the account
+ * exists and has all of the specified features.
*/
- @RequiresPermission(GET_ACCOUNTS)
public AccountManagerFuture<Boolean> hasFeatures(final Account account,
final String[] features,
AccountManagerCallback<Boolean> callback, Handler handler) {
@@ -657,40 +759,42 @@
}
/**
- * Lists all accounts of a type which have certain features. The account
- * type identifies the authenticator (see {@link #getAccountsByType}).
- * Account features are authenticator-specific string tokens identifying
- * boolean account properties (see {@link #hasFeatures}).
+ * Lists all accounts of a type which have certain features. The account type identifies the
+ * authenticator (see {@link #getAccountsByType}). Account features are authenticator-specific
+ * string tokens identifying boolean account properties (see {@link #hasFeatures}).
*
- * <p>Unlike {@link #getAccountsByType}, this method calls the authenticator,
- * which may contact the server or do other work to check account features,
- * so the method returns an {@link AccountManagerFuture}.
+ * <p>
+ * Unlike {@link #getAccountsByType}, this method calls the authenticator, which may contact the
+ * server or do other work to check account features, so the method returns an
+ * {@link AccountManagerFuture}.
*
- * <p>This method may be called from any thread, but the returned
- * {@link AccountManagerFuture} must not be used on the main thread.
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
*
- * <p>Clients of this method that have not been granted the
- * {@link android.Manifest.permission#GET_ACCOUNTS} permission,
- * will only see those accounts managed by AbstractAccountAuthenticators whose
- * signature matches the client.
+ * <p>
+ * Caller targeting API level {@link android.os.Build.VERSION_CODES#O} and above, will get list
+ * of accounts made visible to it by user or AbstractAcccountAuthenticator and
+ * {@link android.Manifest.permission#GET_ACCOUNTS} permission is not used.
+ *
+ * <p>
+ * Caller targeting API level below {@link android.os.Build.VERSION_CODES#O} that have not been
+ * granted the {@link android.Manifest.permission#GET_ACCOUNTS} permission, will only see those
+ * accounts managed by AbstractAccountAuthenticators whose signature matches the client.
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and before, GET_ACCOUNTS permission is
+ * needed for those platforms, irrespective of uid or signature match. See docs for this
+ * function in API level {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
+ *
*
* @param type The type of accounts to return, must not be null
- * @param features An array of the account features to require,
- * may be null or empty
- *
- * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
- * GET_ACCOUNTS permission is needed for those platforms, irrespective of uid
- * or signature match. See docs for this function in API level 22.
- *
- * @param callback Callback to invoke when the request completes,
- * null for no callback
- * @param handler {@link Handler} identifying the callback thread,
- * null for the main thread
- * @return An {@link AccountManagerFuture} which resolves to an array of
- * {@link Account}, one per account of the specified type which
- * matches the requested features.
+ * @param features An array of the account features to require, may be null or empty *
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to an array of {@link Account}, one
+ * per account of the specified type which matches the requested features.
*/
- @RequiresPermission(GET_ACCOUNTS)
public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
final String type, final String[] features,
AccountManagerCallback<Account[]> callback, Handler handler) {
@@ -751,33 +855,70 @@
}
/**
- * Adds an account directly to the AccountManager. Additionally this
- * makes the Account visible to desired UIDs of applications on the device,
- * and sends directed broadcasts to these individual applications.
- * <p>Normally used by sign-up wizards associated with authenticators, not
- * directly by applications.
- * <p>Calling this method does not update the last authenticated timestamp,
- * referred by {@link #KEY_LAST_AUTHENTICATED_TIME}. To update it, call
+ * Adds an account directly to the AccountManager. Additionally this makes the Account visible
+ * to desired UIDs of applications on the device, and sends directed broadcasts to these
+ * individual applications.
+ * <p>
+ * Normally used by sign-up wizards associated with authenticators, not directly by
+ * applications.
+ * <p>
+ * Calling this method does not update the last authenticated timestamp, referred by
+ * {@link #KEY_LAST_AUTHENTICATED_TIME}. To update it, call
* {@link #notifyAccountAuthenticated(Account)} after getting success.
- * <p>It is safe to call this method from the main thread.
- * <p>This method requires the caller to have a signature match with the
- * authenticator that owns the specified account.
+ * <p>
+ * It is safe to call this method from the main thread.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
*
* @param account The {@link Account} to add
* @param password The password to associate with the account, null for none
- * @param extras String values to use for the account's userdata, null for
- * none
- * @param selectedUids Array of uids whose associated applications can access
- * this account without any additional user approval.
+ * @param extras String values to use for the account's userdata, null for none
+ * @param selectedUids Array of uids whose associated applications can access this account
+ * without any additional user approval.
*
- * @return True if the account was successfully added, false if the account
- * already exists, the account is null, or another error occurs.
+ * @return True if the account was successfully added, false if the account already exists, the
+ * account is null, or another error occurs.
*/
public boolean addAccountExplicitly(Account account, String password, Bundle extras,
- int[] selectedUids) {
- if (account == null) throw new IllegalArgumentException("account is null");
+ int[] selectedUids) {
+ return false; // TODO remove this method.
+ }
+
+ /**
+ * Adds an account directly to the AccountManager. Additionally this makes the Account visible
+ * to desired UIDs of applications on the device, and sends directed broadcasts to these
+ * individual applications.
+ * <p>
+ * Normally used by sign-up wizards associated with authenticators, not directly by
+ * applications.
+ * <p>
+ * Calling this method does not update the last authenticated timestamp, referred by
+ * {@link #KEY_LAST_AUTHENTICATED_TIME}. To update it, call
+ * {@link #notifyAccountAuthenticated(Account)} after getting success.
+ * <p>
+ * It is safe to call this method from the main thread.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account The {@link Account} to add
+ * @param password The password to associate with the account, null for none
+ * @param extras String values to use for the account's userdata, null for none
+ * @param visibility Map from uid to visibility values which will be set before account is
+ * added. See getAccountVisibility for possilbe values.
+ *
+ * @return True if the account was successfully added, false if the account already exists, the
+ * account is null, or another error occurs.
+ * @hide
+ */
+ public boolean addAccountExplicitly(Account account, String password, Bundle extras,
+ Map<Integer, Integer> visibility) {
+ if (account == null)
+ throw new IllegalArgumentException("account is null");
try {
- return mService.addAccountExplicitlyWithUid(account, password, extras, selectedUids);
+ return mService.addAccountExplicitlyWithVisibility(account, password, extras,
+ visibility);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -803,9 +944,37 @@
}
/**
+ * Gets all accounts of given type and their visibility for specific package. This method
+ * requires the caller to have a signature match with the authenticator that manages
+ * accountType. It is a helper method which combines calls to {@link #getAccountsByType} by
+ * authenticator and {@link #getAccountVisibility} for every returned account.
+ *
+ * <p>
+ *
+ * @param packageName Package name.
+ * @param accountType Account type.
+ *
+ * @return Map with visibility for all accounts of given type. See {@link #getAccountVisibility}
+ * for possilbe values.
+ * @hide
+ */
+ public Map<Account, Integer> getAccountsAndVisibilityForPackage(String packageName,
+ String accountType) {
+ try {
+ @SuppressWarnings("unchecked")
+ Map<Account, Integer> result = (Map<Account, Integer>) mService
+ .getAccountsAndVisibilityForPackage(packageName, accountType);
+ return result;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Gives a certain UID, represented a application, access to an account
- * <p>This method requires the caller to have a signature match with the authenticator
- * that owns the specified account.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
*
* @param account Account to make visible.
* @param uid The UID of the application to add account access.
@@ -814,18 +983,18 @@
*/
public boolean makeAccountVisible(Account account, int uid) {
try {
- return mService.makeAccountVisible(account, uid);
+ return mService.setAccountVisibility(account, uid, VISIBILITY_USER_MANAGED_VISIBLE);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
- * Removes visibility of certain account of a process identified
- * by a given UID to an application.
- * This is called by the Authenticator.
- * <p>This method requires the caller to have a signature match with the authenticator
- * that owns the specified account.
+ * Removes visibility of certain account of a process identified by a given UID to an
+ * application. This is called by the Authenticator.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
*
* @param account Remove visibility of this account..
* @param uid The UID of the application to remove account access.
@@ -834,17 +1003,18 @@
*/
public boolean removeAccountVisibility(Account account, int uid) {
try {
- return mService.removeAccountVisibility(account, uid);
+ return mService.setAccountVisibility(account, uid, VISIBILITY_USER_MANAGED_NOT_VISIBLE);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
- * Checks visibility of certain account of a process identified
- * by a given UID. This is called by the Authenticator.
- * <p>This method requires the caller to have a signature match with the authenticator
- * that owns the specified account.
+ * Checks visibility of certain account of a process identified by a given UID. This is called
+ * by the Authenticator.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
*
* @param account Account to check visibility.
* @param uid The UID of the application to check account access.
@@ -853,7 +1023,60 @@
*/
public boolean isAccountVisible(Account account, int uid) {
try {
- return mService.isAccountVisible(account, uid);
+ Integer visibility = mService.getAccountVisibility(account, uid);
+ return visibility == VISIBILITY_USER_MANAGED_NOT_VISIBLE
+ || visibility == VISIBILITY_VISIBLE;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set visibility value of given account to certain UID.
+ * <p>
+ * See {@link #getAccountVisibility} for possible values.
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account Account to make visible.
+ * @param uid The UID of the application to modify account visibility.
+ * @param visibility - new visibility value.
+ *
+ * @return True if visibility value was succesfully updated.
+ * @hide
+ */
+ public boolean setAccountVisibility(Account account, int uid,
+ @AccountVisibility int visibility) {
+ try {
+ return mService.setAccountVisibility(account, uid, visibility);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets visibility of certain account for given UID. Possible returned values are:
+ * <ul>
+ * <li>{@link #VISIBILITY_VISIBLE}</li>
+ * <li>{@link #VISIBILITY_USER_MANAGED_VISIBLE}</li>
+ * <li>{@link #VISIBILITY_NOT_VISIBLE}
+ * <li>{@link #VISIBILITY_USER_MANAGED_NOT_VISIBLE}</li>
+ * </ul>
+ *
+ * <p>
+ * This method requires the caller to have a signature match with the authenticator that owns
+ * the specified account.
+ *
+ * @param account Account to get visibility.
+ * @param uid The UID of the application to get account visibility.
+ *
+ * @return int Visibility for given account and uid.
+ * @hide
+ */
+ public @AccountVisibility int getAccountVisibility(Account account, int uid) {
+ try {
+ return mService.getAccountVisibility(account, uid);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -2428,6 +2651,9 @@
};
// have many accounts, launch the chooser
Intent intent = new Intent();
+ // TODO - this activity will not include
+ // USER_MANAGED_NOT_VISIBLE
+ // accounts. We need to move method to service
ComponentName componentName = ComponentName.unflattenFromString(
Resources.getSystem().getString(
R.string.config_chooseAccountActivity));
@@ -2483,58 +2709,57 @@
}
/**
- * This convenience helper combines the functionality of
- * {@link #getAccountsByTypeAndFeatures}, {@link #getAuthToken}, and
- * {@link #addAccount}.
+ * This convenience helper combines the functionality of {@link #getAccountsByTypeAndFeatures},
+ * {@link #getAuthToken}, and {@link #addAccount}.
*
- * <p>This method gets a list of the accounts matching the
- * specified type and feature set; if there is exactly one, it is
- * used; if there are more than one, the user is prompted to pick one;
- * if there are none, the user is prompted to add one. Finally,
- * an auth token is acquired for the chosen account.
+ * <p>
+ * This method gets a list of the accounts matching specific type and feature set which are
+ * visible to the caller or for which user can grant access (see {@link #getAccountsByType} for
+ * details); if there is exactly one already visible account, it is used; if there are some
+ * accounts for which user grant visibility, the user is prompted to pick one; if there are
+ * none, the user is prompted to add one. Finally, an auth token is acquired for the chosen
+ * account.
*
- * <p>This method may be called from any thread, but the returned
- * {@link AccountManagerFuture} must not be used on the main thread.
+ * <p>
+ * This method may be called from any thread, but the returned {@link AccountManagerFuture} must
+ * not be used on the main thread.
*
- * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before,
- * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for
- * this function in API level 22.
+ * <p>
+ * <b>NOTE:</b> If targeting your app to work on API level 22 and before, MANAGE_ACCOUNTS
+ * permission is needed for those platforms. See docs for this function in API level 22.
*
- * @param accountType The account type required
- * (see {@link #getAccountsByType}), must not be null
- * @param authTokenType The desired auth token type
- * (see {@link #getAuthToken}), must not be null
- * @param features Required features for the account
- * (see {@link #getAccountsByTypeAndFeatures}), may be null or empty
- * @param activity The {@link Activity} context to use for launching new
- * sub-Activities to prompt to add an account, select an account,
- * and/or enter a password, as necessary; used only to call
- * startActivity(); should not be null
- * @param addAccountOptions Authenticator-specific options to use for
- * adding new accounts; may be null or empty
- * @param getAuthTokenOptions Authenticator-specific options to use for
- * getting auth tokens; may be null or empty
- * @param callback Callback to invoke when the request completes,
- * null for no callback
- * @param handler {@link Handler} identifying the callback thread,
- * null for the main thread
- * @return An {@link AccountManagerFuture} which resolves to a Bundle with
- * at least the following fields:
- * <ul>
- * <li> {@link #KEY_ACCOUNT_NAME} - the name of the account
- * <li> {@link #KEY_ACCOUNT_TYPE} - the type of the account
- * <li> {@link #KEY_AUTHTOKEN} - the auth token you wanted
- * </ul>
+ * @param accountType The account type required (see {@link #getAccountsByType}), must not be
+ * null
+ * @param authTokenType The desired auth token type (see {@link #getAuthToken}), must not be
+ * null
+ * @param features Required features for the account (see
+ * {@link #getAccountsByTypeAndFeatures}), may be null or empty
+ * @param activity The {@link Activity} context to use for launching new sub-Activities to
+ * prompt to add an account, select an account, and/or enter a password, as necessary;
+ * used only to call startActivity(); should not be null
+ * @param addAccountOptions Authenticator-specific options to use for adding new accounts; may
+ * be null or empty
+ * @param getAuthTokenOptions Authenticator-specific options to use for getting auth tokens; may
+ * be null or empty
+ * @param callback Callback to invoke when the request completes, null for no callback
+ * @param handler {@link Handler} identifying the callback thread, null for the main thread
+ * @return An {@link AccountManagerFuture} which resolves to a Bundle with at least the
+ * following fields:
+ * <ul>
+ * <li>{@link #KEY_ACCOUNT_NAME} - the name of the account
+ * <li>{@link #KEY_ACCOUNT_TYPE} - the type of the account
+ * <li>{@link #KEY_AUTHTOKEN} - the auth token you wanted
+ * </ul>
*
- * If an error occurred, {@link AccountManagerFuture#getResult()} throws:
- * <ul>
- * <li> {@link AuthenticatorException} if no authenticator was registered for
- * this account type or the authenticator failed to respond
- * <li> {@link OperationCanceledException} if the operation was canceled for
- * any reason, including the user canceling any operation
- * <li> {@link IOException} if the authenticator experienced an I/O problem
- * updating settings, usually because of network trouble
- * </ul>
+ * If an error occurred, {@link AccountManagerFuture#getResult()} throws:
+ * <ul>
+ * <li>{@link AuthenticatorException} if no authenticator was registered for this
+ * account type or the authenticator failed to respond
+ * <li>{@link OperationCanceledException} if the operation was canceled for any reason,
+ * including the user canceling any operation
+ * <li>{@link IOException} if the authenticator experienced an I/O problem updating
+ * settings, usually because of network trouble
+ * </ul>
*/
public AccountManagerFuture<Bundle> getAuthTokenByFeatures(
final String accountType, final String authTokenType, final String[] features,
@@ -2689,33 +2914,33 @@
};
/**
- * Adds an {@link OnAccountsUpdateListener} to this instance of the
- * {@link AccountManager}. This listener will be notified whenever the
- * list of accounts on the device changes.
+ * Adds an {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}. This
+ * listener will be notified whenever user or AbstractAcccountAuthenticator made changes to
+ * accounts related to the caller - either list of accounts returned by {@link #getAccounts()}
+ * was changed, or new account was added for which user can grant access to the caller.
*
- * <p>As long as this listener is present, the AccountManager instance
- * will not be garbage-collected, and neither will the {@link Context}
- * used to retrieve it, which may be a large Activity instance. To avoid
- * memory leaks, you must remove this listener before then. Normally
- * listeners are added in an Activity or Service's {@link Activity#onCreate}
- * and removed in {@link Activity#onDestroy}.
+ * <p>
+ * As long as this listener is present, the AccountManager instance will not be
+ * garbage-collected, and neither will the {@link Context} used to retrieve it, which may be a
+ * large Activity instance. To avoid memory leaks, you must remove this listener before then.
+ * Normally listeners are added in an Activity or Service's {@link Activity#onCreate} and
+ * removed in {@link Activity#onDestroy}.
*
- * <p>The listener will only be informed of accounts that would be returned
- * to the caller via {@link #getAccounts()}. Typically this means that to
- * get any accounts, the caller will need to be grated the GET_ACCOUNTS
- * permission.
*
- * <p>It is safe to call this method from the main thread.
+ * If SUPPORTED_ACCOUNT_TYPES is specified in the manifest file, listener will only be
+ * notified about whitelisted types.
+ *
+ * <p>
+ * It is safe to call this method from the main thread.
*
* @param listener The listener to send notifications to
- * @param handler {@link Handler} identifying the thread to use
- * for notifications, null for the main thread
- * @param updateImmediately If true, the listener will be invoked
- * (on the handler thread) right away with the current account list
+ * @param handler {@link Handler} identifying the thread to use for notifications, null for the
+ * main thread
+ * @param updateImmediately If true, the listener will be invoked (on the handler thread) right
+ * away with the current account list
* @throws IllegalArgumentException if listener is null
* @throws IllegalStateException if listener was already added
*/
- @RequiresPermission(GET_ACCOUNTS)
public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener,
Handler handler, boolean updateImmediately) {
if (listener == null) {
@@ -2729,22 +2954,47 @@
mAccountsUpdatedListeners.put(listener, handler);
+
if (wasEmpty) {
// Register a broadcast receiver to monitor account changes
IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION);
+ if (isVisibleAccountsChangedBroadcastSupported()) {
+ intentFilter.addAction(ACTION_VISIBLE_ACCOUNTS_CHANGED);
+ } else {
+ intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION);
+ }
// To recover from disk-full.
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ // Register a broadcast receiver to monitor account changes
mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
}
}
-
if (updateImmediately) {
postToHandler(handler, listener, getAccounts());
}
}
/**
+ * @hide
+ */
+ private boolean isVisibleAccountsChangedBroadcastSupported() {
+ String interestedTypes = null;
+ try {
+ String packageName = mContext.getOpPackageName();
+ ApplicationInfo ai = mContext.getPackageManager().getApplicationInfo(packageName,
+ PackageManager.GET_META_DATA);
+ Bundle b = ai.metaData;
+ if (b == null) {
+ return false;
+ }
+ interestedTypes = b.getString(SUPPORTED_ACCOUNT_TYPES);
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ return !TextUtils.isEmpty(interestedTypes);
+ }
+
+ /**
* Removes an {@link OnAccountsUpdateListener} previously registered with
* {@link #addOnAccountsUpdatedListener}. The listener will no longer
* receive notifications of account changes.
diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl
index fc10990..66c3ca3 100644
--- a/core/java/android/accounts/IAccountManager.aidl
+++ b/core/java/android/accounts/IAccountManager.aidl
@@ -24,6 +24,8 @@
import android.os.RemoteCallback;
import android.os.UserHandle;
+import java.util.Map;
+
/**
* Central application service that provides account management.
* @hide
@@ -106,18 +108,17 @@
void isCredentialsUpdateSuggested(in IAccountManagerResponse response, in Account account,
String statusToken);
- /* Allows Authenticator to view what packages or UIDs on phone have requested it. */
+ /* Allows Authenticator to get UIDs of packages which registered to receive updates about given account type.*/
int[] getRequestingUidsForType(String accountType);
- /* Allows authenticator to add an account explicitly that is only visible to
- certain uids; the authenticator learns of these UIDs */
- boolean addAccountExplicitlyWithUid(in Account account, String password, in Bundle extras,
- in int[] selectedUids);
+ boolean addAccountExplicitlyWithVisibility(in Account account, String password, in Bundle extras,
+ in Map visibility);
- /* Controls visibility of UIDs of applications to Accounts */
- boolean removeAccountVisibility(in Account a, in int uid);
- boolean makeAccountVisible(in Account a, in int uid);
- boolean isAccountVisible(in Account a, in int uid);
+ boolean setAccountVisibility(in Account a, int uid, int newVisibility);
+ int getAccountVisibility(in Account a, int uid);
+
+ /* Type may be null returns Map <Account, Integer>*/
+ Map getAccountsAndVisibilityForPackage(in String packageName, in String accountType);
/* Check if the package in a user can access an account */
boolean hasAccountAccess(in Account account, String packageName, in UserHandle userHandle);
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index 1ba68a6..2590a6b 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -221,4 +221,9 @@
public abstract void requestEphemeralResolutionPhaseTwo(EphemeralResponse responseObj,
Intent origIntent, String resolvedType, Intent launchIntent, String callingPackage,
int userId);
+
+ /**
+ * @return The SetupWizard package name.
+ */
+ public abstract String getSetupWizardPackageName();
}
diff --git a/core/java/android/net/RecommendationResult.java b/core/java/android/net/RecommendationResult.java
index a330d84..70cf09c 100644
--- a/core/java/android/net/RecommendationResult.java
+++ b/core/java/android/net/RecommendationResult.java
@@ -16,6 +16,7 @@
package android.net;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.net.wifi.WifiConfiguration;
@@ -23,6 +24,7 @@
import android.os.Parcelable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
/**
* The result of a network recommendation.
@@ -34,7 +36,32 @@
public final class RecommendationResult implements Parcelable {
private final WifiConfiguration mWifiConfiguration;
- public RecommendationResult(@Nullable WifiConfiguration wifiConfiguration) {
+ /**
+ * Create a {@link RecommendationResult} that indicates that no network connection should be
+ * attempted at this time.
+ *
+ * @return a {@link RecommendationResult}
+ */
+ public static RecommendationResult createDoNotConnectRecommendation() {
+ return new RecommendationResult((WifiConfiguration) null);
+ }
+
+ /**
+ * Create a {@link RecommendationResult} that indicates that a connection attempt should be
+ * made for the given Wi-Fi network.
+ *
+ * @param wifiConfiguration {@link WifiConfiguration} with at least SSID and BSSID set.
+ * @return a {@link RecommendationResult}
+ */
+ public static RecommendationResult createConnectRecommendation(
+ @NonNull WifiConfiguration wifiConfiguration) {
+ Preconditions.checkNotNull(wifiConfiguration, "wifiConfiguration must not be null");
+ Preconditions.checkNotNull(wifiConfiguration.SSID, "SSID must not be null");
+ Preconditions.checkNotNull(wifiConfiguration.BSSID, "BSSID must not be null");
+ return new RecommendationResult(wifiConfiguration);
+ }
+
+ private RecommendationResult(@Nullable WifiConfiguration wifiConfiguration) {
mWifiConfiguration = wifiConfiguration;
}
@@ -43,14 +70,29 @@
}
/**
- * @return The recommended {@link WifiConfiguration} to connect to. A {@code null} value
- * indicates that no WiFi connection should be attempted at this time.
+ * @return {@code true} if a network recommendation exists. {@code false} indicates that
+ * no connection should be attempted at this time.
*/
- public WifiConfiguration getWifiConfiguration() {
+ public boolean hasRecommendation() {
+ return mWifiConfiguration != null;
+ }
+
+ /**
+ * @return The recommended {@link WifiConfiguration} to connect to. A {@code null} value
+ * is returned if {@link #hasRecommendation} returns {@code false}.
+ */
+ @Nullable public WifiConfiguration getWifiConfiguration() {
return mWifiConfiguration;
}
@Override
+ public String toString() {
+ return "RecommendationResult{" +
+ "mWifiConfiguration=" + mWifiConfiguration +
+ "}";
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 37222ad..488c228 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -16,11 +16,17 @@
package android.provider;
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
import android.annotation.TestApi;
+import android.annotation.UserIdInt;
import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.app.Application;
@@ -66,6 +72,8 @@
import com.android.internal.widget.ILockSettings;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
@@ -347,7 +355,6 @@
* Input: Nothing.
* <p>
* Output: Nothing.
-
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_WIFI_SETTINGS =
@@ -1065,8 +1072,6 @@
/**
* Activity Action: Show Zen Mode priority configuration settings.
- *
- * @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_ZEN_MODE_PRIORITY_SETTINGS
@@ -1211,8 +1216,6 @@
public static final String ACTION_HOME_SETTINGS
= "android.settings.HOME_SETTINGS";
-
-
/**
* Activity Action: Show Default apps settings.
* <p>
@@ -1387,6 +1390,21 @@
*/
public static final String CALL_METHOD_USER_KEY = "_user";
+ /**
+ * @hide - Boolean argument extra to the fast-path call()-based requests
+ */
+ public static final String CALL_METHOD_MAKE_DEFAULT_KEY = "_make_default";
+
+ /**
+ * @hide - User handle argument extra to the fast-path call()-based requests
+ */
+ public static final String CALL_METHOD_RESET_MODE_KEY = "_reset_mode";
+
+ /**
+ * @hide - String argument extra to the fast-path call()-based requests
+ */
+ public static final String CALL_METHOD_TAG_KEY = "_tag";
+
/** @hide - Private call() method to write to 'system' table */
public static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
@@ -1396,6 +1414,12 @@
/** @hide - Private call() method to write to 'global' table */
public static final String CALL_METHOD_PUT_GLOBAL= "PUT_global";
+ /** @hide - Private call() method to reset to defaults the 'global' table */
+ public static final String CALL_METHOD_RESET_GLOBAL = "RESET_global";
+
+ /** @hide - Private call() method to reset to defaults the 'secure' table */
+ public static final String CALL_METHOD_RESET_SECURE = "RESET_secure";
+
/**
* Activity Extra: Limit available options in launched activity based on the given authority.
* <p>
@@ -1472,6 +1496,55 @@
"android.settings.extra.do_not_disturb_mode_minutes";
/**
+ * Reset mode: reset to defaults only settings changed by the
+ * calling package. If there is a default set the setting
+ * will be set to it, otherwise the setting will be deleted.
+ * This is the only type of reset available to non-system clients.
+ * @hide
+ */
+ public static final int RESET_MODE_PACKAGE_DEFAULTS = 1;
+
+ /**
+ * Reset mode: reset all settings set by untrusted packages, which is
+ * packages that aren't a part of the system, to the current defaults.
+ * If there is a default set the setting will be set to it, otherwise
+ * the setting will be deleted. This mode is only available to the system.
+ * @hide
+ */
+ public static final int RESET_MODE_UNTRUSTED_DEFAULTS = 2;
+
+ /**
+ * Reset mode: delete all settings set by untrusted packages, which is
+ * packages that aren't a part of the system. If a setting is set by an
+ * untrusted package it will be deleted if its default is not provided
+ * by the system, otherwise the setting will be set to its default.
+ * This mode is only available to the system.
+ * @hide
+ */
+ public static final int RESET_MODE_UNTRUSTED_CHANGES = 3;
+
+ /**
+ * Reset mode: reset all settings to defaults specified by trusted
+ * packages, which is packages that are a part of the system, and
+ * delete all settings set by untrusted packages. If a setting has
+ * a default set by a system package it will be set to the default,
+ * otherwise the setting will be deleted. This mode is only available
+ * to the system.
+ * @hide
+ */
+ public static final int RESET_MODE_TRUSTED_DEFAULTS = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RESET_MODE_PACKAGE_DEFAULTS,
+ RESET_MODE_UNTRUSTED_DEFAULTS,
+ RESET_MODE_UNTRUSTED_CHANGES,
+ RESET_MODE_TRUSTED_DEFAULTS
+ })
+ public @interface ResetMode{}
+
+ /**
* Activity Extra: Number of certificates
* <p>
* This can be passed as an extra field to the {@link #ACTION_MONITORING_CERT_INFO}
@@ -1574,22 +1647,44 @@
}
}
+ private static final class ContentProviderHolder {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final Uri mUri;
+ @GuardedBy("mLock")
+ private IContentProvider mContentProvider;
+
+ public ContentProviderHolder(Uri uri) {
+ mUri = uri;
+ }
+
+ public IContentProvider getProvider(ContentResolver contentResolver) {
+ synchronized (mLock) {
+ if (mContentProvider == null) {
+ mContentProvider = contentResolver
+ .acquireProvider(mUri.getAuthority());
+ }
+ return mContentProvider;
+ }
+ }
+ }
+
// Thread-safe.
private static class NameValueCache {
private static final boolean DEBUG = false;
- private final Uri mUri;
-
private static final String[] SELECT_VALUE_PROJECTION = new String[] {
Settings.NameValueTable.VALUE
};
+
private static final String NAME_EQ_PLACEHOLDER = "name=?";
// Must synchronize on 'this' to access mValues and mValuesVersion.
private final HashMap<String, String> mValues = new HashMap<>();
- // Initially null; set lazily and held forever. Synchronized on 'this'.
- private IContentProvider mContentProvider = null;
+ private final Uri mUri;
+ private final ContentProviderHolder mProviderHolder;
// The method we'll call (or null, to not use) on the provider
// for the fast path of retrieving settings.
@@ -1599,30 +1694,27 @@
@GuardedBy("this")
private GenerationTracker mGenerationTracker;
- public NameValueCache(Uri uri, String getCommand, String setCommand) {
+ public NameValueCache(Uri uri, String getCommand, String setCommand,
+ ContentProviderHolder providerHolder) {
mUri = uri;
mCallGetCommand = getCommand;
mCallSetCommand = setCommand;
- }
-
- private IContentProvider lazyGetProvider(ContentResolver cr) {
- IContentProvider cp = null;
- synchronized (NameValueCache.this) {
- cp = mContentProvider;
- if (cp == null) {
- cp = mContentProvider = cr.acquireProvider(mUri.getAuthority());
- }
- }
- return cp;
+ mProviderHolder = providerHolder;
}
public boolean putStringForUser(ContentResolver cr, String name, String value,
- final int userHandle) {
+ String tag, boolean makeDefault, final int userHandle) {
try {
Bundle arg = new Bundle();
arg.putString(Settings.NameValueTable.VALUE, value);
arg.putInt(CALL_METHOD_USER_KEY, userHandle);
- IContentProvider cp = lazyGetProvider(cr);
+ if (tag != null) {
+ arg.putString(CALL_METHOD_TAG_KEY, tag);
+ }
+ if (makeDefault) {
+ arg.putBoolean(CALL_METHOD_MAKE_DEFAULT_KEY, true);
+ }
+ IContentProvider cp = mProviderHolder.getProvider(cr);
cp.call(cr.getPackageName(), mCallSetCommand, name, arg);
} catch (RemoteException e) {
Log.w(TAG, "Can't set key " + name + " in " + mUri, e);
@@ -1653,7 +1745,7 @@
+ " by user " + UserHandle.myUserId() + " so skipping cache");
}
- IContentProvider cp = lazyGetProvider(cr);
+ IContentProvider cp = mProviderHolder.getProvider(cr);
// Try the fast path first, not using query(). If this
// fails (alternate Settings provider that doesn't support
@@ -1802,10 +1894,14 @@
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY + "/system");
+ private static final ContentProviderHolder sProviderHolder =
+ new ContentProviderHolder(CONTENT_URI);
+
private static final NameValueCache sNameValueCache = new NameValueCache(
CONTENT_URI,
CALL_METHOD_GET_SYSTEM,
- CALL_METHOD_PUT_SYSTEM);
+ CALL_METHOD_PUT_SYSTEM,
+ sProviderHolder);
private static final HashSet<String> MOVED_TO_SECURE;
static {
@@ -1997,7 +2093,7 @@
+ " to android.provider.Settings.Global, value is unchanged.");
return false;
}
- return sNameValueCache.putStringForUser(resolver, name, value, userHandle);
+ return sNameValueCache.putStringForUser(resolver, name, value, null, false, userHandle);
}
/**
@@ -4153,11 +4249,15 @@
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY + "/secure");
+ private static final ContentProviderHolder sProviderHolder =
+ new ContentProviderHolder(CONTENT_URI);
+
// Populated lazily, guarded by class object:
private static final NameValueCache sNameValueCache = new NameValueCache(
CONTENT_URI,
CALL_METHOD_GET_SECURE,
- CALL_METHOD_PUT_SECURE);
+ CALL_METHOD_PUT_SECURE,
+ sProviderHolder);
private static ILockSettings sLockSettings = null;
@@ -4357,6 +4457,13 @@
/** @hide */
public static boolean putStringForUser(ContentResolver resolver, String name, String value,
int userHandle) {
+ return putStringForUser(resolver, name, value, null, false, userHandle);
+ }
+
+ /** @hide */
+ public static boolean putStringForUser(@NonNull ContentResolver resolver,
+ @NonNull String name, @Nullable String value, @Nullable String tag,
+ boolean makeDefault, @UserIdInt int userHandle) {
if (LOCATION_MODE.equals(name)) {
// Map LOCATION_MODE to underlying location provider storage API
return setLocationModeForUser(resolver, Integer.parseInt(value), userHandle);
@@ -4364,9 +4471,113 @@
if (MOVED_TO_GLOBAL.contains(name)) {
Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System"
+ " to android.provider.Settings.Global");
- return Global.putStringForUser(resolver, name, value, userHandle);
+ return Global.putStringForUser(resolver, name, value,
+ tag, makeDefault, userHandle);
}
- return sNameValueCache.putStringForUser(resolver, name, value, userHandle);
+ return sNameValueCache.putStringForUser(resolver, name, value, tag,
+ makeDefault, userHandle);
+ }
+
+ /**
+ * Store a name/value pair into the database.
+ * <p>
+ * The method takes an optional tag to associate with the setting
+ * which can be used to clear only settings made by your package and
+ * associated with this tag by passing the tag to {@link
+ * #resetToDefaults(ContentResolver, String)}. Anyone can override
+ * the current tag. Also if another package changes the setting
+ * then the tag will be set to the one specified in the set call
+ * which can be null. Also any of the settings setters that do not
+ * take a tag as an argument effectively clears the tag.
+ * </p><p>
+ * For example, if you set settings A and B with tags T1 and T2 and
+ * another app changes setting A (potentially to the same value), it
+ * can assign to it a token T3 (note that now the package that changed
+ * the setting is not yours). Now if you reset your changes for T1 and
+ * T2 only setting B will be reset and A not (as it was changed by
+ * another package) but since A did not change you are in the desired
+ * initial state. Now if the other app changes the value of A (assuming
+ * you registered an observer in the beginning) you would detect that
+ * the setting was changed by another app and handle this appropriately
+ * (ignore, set back to some value, etc).
+ * </p><p>
+ * Also the method takes an argument whether to make the value the
+ * default for this setting. If the system already specified a default
+ * value, then the one passed in here will <strong>not</strong>
+ * be set as the default.
+ * </p>
+ *
+ * @param resolver to access the database with.
+ * @param name to store.
+ * @param value to associate with the name.
+ * @param tag to associate with the setting.
+ * @param makeDefault whether to make the value the default one.
+ * @return true if the value was set, false on database errors.
+ *
+ * @see #resetToDefaults(ContentResolver, String)
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public static boolean putString(@NonNull ContentResolver resolver,
+ @NonNull String name, @Nullable String value, @Nullable String tag,
+ boolean makeDefault) {
+ return putStringForUser(resolver, name, value, tag, makeDefault,
+ UserHandle.myUserId());
+ }
+
+ /**
+ * Reset the settings to their defaults. This would reset <strong>only</strong>
+ * settings set by the caller's package. Think of it of a way to undo your own
+ * changes to the global settings. Passing in the optional tag will reset only
+ * settings changed by your package and associated with this tag.
+ *
+ * @param resolver Handle to the content resolver.
+ * @param tag Optional tag which should be associated with the settings to reset.
+ *
+ * @see #putString(ContentResolver, String, String, String, boolean)
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void resetToDefaults(@NonNull ContentResolver resolver,
+ @Nullable String tag) {
+ resetToDefaultsAsUser(resolver, tag, RESET_MODE_PACKAGE_DEFAULTS,
+ UserHandle.myUserId());
+ }
+
+ /**
+ * Reset the settings to their defaults for a given user with a specific mode. The
+ * optional tag argument is valid only for {@link #RESET_MODE_PACKAGE_DEFAULTS}
+ * allowing resetting the settings made by a package and associated with the tag.
+ *
+ * @param resolver Handle to the content resolver.
+ * @param tag Optional tag which should be associated with the settings to reset.
+ * @param mode The reset mode.
+ * @param userHandle The user for which to reset to defaults.
+ *
+ * @see #RESET_MODE_PACKAGE_DEFAULTS
+ * @see #RESET_MODE_UNTRUSTED_DEFAULTS
+ * @see #RESET_MODE_UNTRUSTED_CHANGES
+ * @see #RESET_MODE_TRUSTED_DEFAULTS
+ *
+ * @hide
+ */
+ public static void resetToDefaultsAsUser(@NonNull ContentResolver resolver,
+ @Nullable String tag, @ResetMode int mode, @IntRange(from = 0) int userHandle) {
+ try {
+ Bundle arg = new Bundle();
+ arg.putInt(CALL_METHOD_USER_KEY, userHandle);
+ if (tag != null) {
+ arg.putString(CALL_METHOD_TAG_KEY, tag);
+ }
+ arg.putInt(CALL_METHOD_RESET_MODE_KEY, mode);
+ IContentProvider cp = sProviderHolder.getProvider(resolver);
+ cp.call(resolver.getPackageName(), CALL_METHOD_RESET_SECURE, null, arg);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Can't reset do defaults for " + CONTENT_URI, e);
+ }
}
/**
@@ -9133,11 +9344,15 @@
LOW_POWER_MODE_TRIGGER_LEVEL
};
+ private static final ContentProviderHolder sProviderHolder =
+ new ContentProviderHolder(CONTENT_URI);
+
// Populated lazily, guarded by class object:
- private static NameValueCache sNameValueCache = new NameValueCache(
+ private static final NameValueCache sNameValueCache = new NameValueCache(
CONTENT_URI,
CALL_METHOD_GET_GLOBAL,
- CALL_METHOD_PUT_GLOBAL);
+ CALL_METHOD_PUT_GLOBAL,
+ sProviderHolder);
// Certain settings have been moved from global to the per-user secure namespace
private static final HashSet<String> MOVED_TO_SECURE;
@@ -9181,12 +9396,121 @@
*/
public static boolean putString(ContentResolver resolver,
String name, String value) {
- return putStringForUser(resolver, name, value, UserHandle.myUserId());
+ return putStringForUser(resolver, name, value, null, false, UserHandle.myUserId());
+ }
+
+ /**
+ * Store a name/value pair into the database.
+ * <p>
+ * The method takes an optional tag to associate with the setting
+ * which can be used to clear only settings made by your package and
+ * associated with this tag by passing the tag to {@link
+ * #resetToDefaults(ContentResolver, String)}. Anyone can override
+ * the current tag. Also if another package changes the setting
+ * then the tag will be set to the one specified in the set call
+ * which can be null. Also any of the settings setters that do not
+ * take a tag as an argument effectively clears the tag.
+ * </p><p>
+ * For example, if you set settings A and B with tags T1 and T2 and
+ * another app changes setting A (potentially to the same value), it
+ * can assign to it a token T3 (note that now the package that changed
+ * the setting is not yours). Now if you reset your changes for T1 and
+ * T2 only setting B will be reset and A not (as it was changed by
+ * another package) but since A did not change you are in the desired
+ * initial state. Now if the other app changes the value of A (assuming
+ * you registered an observer in the beginning) you would detect that
+ * the setting was changed by another app and handle this appropriately
+ * (ignore, set back to some value, etc).
+ * </p><p>
+ * Also the method takes an argument whether to make the value the
+ * default for this setting. If the system already specified a default
+ * value, then the one passed in here will <strong>not</strong>
+ * be set as the default.
+ * </p>
+ *
+ * @param resolver to access the database with.
+ * @param name to store.
+ * @param value to associate with the name.
+ * @param tag to associated with the setting.
+ * @param makeDefault whether to make the value the default one.
+ * @return true if the value was set, false on database errors.
+ *
+ * @see #resetToDefaults(ContentResolver, String)
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public static boolean putString(@NonNull ContentResolver resolver,
+ @NonNull String name, @Nullable String value, @Nullable String tag,
+ boolean makeDefault) {
+ return putStringForUser(resolver, name, value, tag, makeDefault,
+ UserHandle.myUserId());
+ }
+
+ /**
+ * Reset the settings to their defaults. This would reset <strong>only</strong>
+ * settings set by the caller's package. Think of it of a way to undo your own
+ * changes to the secure settings. Passing in the optional tag will reset only
+ * settings changed by your package and associated with this tag.
+ *
+ * @param resolver Handle to the content resolver.
+ * @param tag Optional tag which should be associated with the settings to reset.
+ *
+ * @see #putString(ContentResolver, String, String, String, boolean)
+ *
+ * @hide
+ */
+ @SystemApi
+ public static void resetToDefaults(@NonNull ContentResolver resolver,
+ @Nullable String tag) {
+ resetToDefaultsAsUser(resolver, tag, RESET_MODE_PACKAGE_DEFAULTS,
+ UserHandle.myUserId());
+ }
+
+ /**
+ * Reset the settings to their defaults for a given user with a specific mode. The
+ * optional tag argument is valid only for {@link #RESET_MODE_PACKAGE_DEFAULTS}
+ * allowing resetting the settings made by a package and associated with the tag.
+ *
+ * @param resolver Handle to the content resolver.
+ * @param tag Optional tag which should be associated with the settings to reset.
+ * @param mode The reset mode.
+ * @param userHandle The user for which to reset to defaults.
+ *
+ * @see #RESET_MODE_PACKAGE_DEFAULTS
+ * @see #RESET_MODE_UNTRUSTED_DEFAULTS
+ * @see #RESET_MODE_UNTRUSTED_CHANGES
+ * @see #RESET_MODE_TRUSTED_DEFAULTS
+ *
+ * @hide
+ */
+ public static void resetToDefaultsAsUser(@NonNull ContentResolver resolver,
+ @Nullable String tag, @ResetMode int mode, @IntRange(from = 0) int userHandle) {
+ try {
+ Bundle arg = new Bundle();
+ arg.putInt(CALL_METHOD_USER_KEY, userHandle);
+ if (tag != null) {
+ arg.putString(CALL_METHOD_TAG_KEY, tag);
+ }
+ arg.putInt(CALL_METHOD_RESET_MODE_KEY, mode);
+ IContentProvider cp = sProviderHolder.getProvider(resolver);
+ cp.call(resolver.getPackageName(), CALL_METHOD_RESET_GLOBAL, null, arg);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Can't reset do defaults for " + CONTENT_URI, e);
+ }
}
/** @hide */
public static boolean putStringForUser(ContentResolver resolver,
String name, String value, int userHandle) {
+ return putStringForUser(resolver, name, value, null, false, userHandle);
+ }
+
+ /** @hide */
+ public static boolean putStringForUser(@NonNull ContentResolver resolver,
+ @NonNull String name, @Nullable String value, @Nullable String tag,
+ boolean makeDefault, @UserIdInt int userHandle) {
if (LOCAL_LOGV) {
Log.v(TAG, "Global.putString(name=" + name + ", value=" + value
+ " for " + userHandle);
@@ -9195,9 +9519,11 @@
if (MOVED_TO_SECURE.contains(name)) {
Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.Global"
+ " to android.provider.Settings.Secure, value is unchanged.");
- return Secure.putStringForUser(resolver, name, value, userHandle);
+ return Secure.putStringForUser(resolver, name, value, tag,
+ makeDefault, userHandle);
}
- return sNameValueCache.putStringForUser(resolver, name, value, userHandle);
+ return sNameValueCache.putStringForUser(resolver, name, value, tag,
+ makeDefault, userHandle);
}
/**
@@ -9418,7 +9744,6 @@
return putString(cr, name, Float.toString(value));
}
-
/**
* Subscription to be used for voice call on a multi sim device. The supported values
* are 0 = SUB1, 1 = SUB2 and etc.
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 1cfbd97..b57f2362 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -240,7 +240,7 @@
t_pri = getpriority(PRIO_PROCESS, t_pid);
if (t_pri <= ANDROID_PRIORITY_AUDIO) {
- int scheduler = sched_getscheduler(t_pid);
+ int scheduler = sched_getscheduler(t_pid) & ~SCHED_RESET_ON_FORK;
if ((scheduler == SCHED_FIFO) || (scheduler == SCHED_RR)) {
// This task wants to stay in its current audio group so it can keep its budget
// don't update its cpuset or cgroup
diff --git a/core/proto/android/content/component_name.proto b/core/proto/android/content/component_name.proto
index 7908af9..90f6ffb 100644
--- a/core/proto/android/content/component_name.proto
+++ b/core/proto/android/content/component_name.proto
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-syntax = "proto2";
+syntax = "proto3";
option java_package = "android.content";
option java_multiple_files = true;
@@ -25,7 +25,7 @@
* An android.content.ComponentName object.
*/
message ComponentNameProto {
- optional string package_name = 1;
- optional string class_name = 2;
+ string package_name = 1;
+ string class_name = 2;
}
diff --git a/core/proto/android/content/configuration.proto b/core/proto/android/content/configuration.proto
index 683e7aa..f46b73a 100644
--- a/core/proto/android/content/configuration.proto
+++ b/core/proto/android/content/configuration.proto
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-syntax = "proto2";
+syntax = "proto3";
option java_package = "android.content";
option java_multiple_files = true;
@@ -27,21 +27,21 @@
* An android resource configuration.
*/
message ConfigurationProto {
- optional float font_scale = 1;
- optional uint32 mcc = 2;
- optional uint32 mnc = 3;
+ float font_scale = 1;
+ uint32 mcc = 2;
+ uint32 mnc = 3;
repeated LocaleProto locales = 4;
- optional uint32 screen_layout = 5;
- optional uint32 touchscreen = 6;
- optional uint32 keyboard_hidden = 7;
- optional uint32 hard_keyboard_hidden = 8;
- optional uint32 navigation = 9;
- optional uint32 navigation_hidden = 10;
- optional uint32 orientation = 11;
- optional uint32 ui_mode = 12;
- optional uint32 screen_width_dp = 13;
- optional uint32 screen_height_dp = 14;
- optional uint32 smallest_screen_width_dp = 15;
- optional uint32 density_dpi = 16;
+ uint32 screen_layout = 5;
+ uint32 touchscreen = 6;
+ uint32 keyboard_hidden = 7;
+ uint32 hard_keyboard_hidden = 8;
+ uint32 navigation = 9;
+ uint32 navigation_hidden = 10;
+ uint32 orientation = 11;
+ uint32 ui_mode = 12;
+ uint32 screen_width_dp = 13;
+ uint32 screen_height_dp = 14;
+ uint32 smallest_screen_width_dp = 15;
+ uint32 density_dpi = 16;
}
diff --git a/core/proto/android/content/locale.proto b/core/proto/android/content/locale.proto
index 55ce68e..961b10b 100644
--- a/core/proto/android/content/locale.proto
+++ b/core/proto/android/content/locale.proto
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-syntax = "proto2";
+syntax = "proto3";
option java_package = "android.content";
option java_multiple_files = true;
@@ -22,8 +22,8 @@
package android.content;
message LocaleProto {
- optional string language = 1;
- optional string country = 2;
- optional string variant = 3;
+ string language = 1;
+ string country = 2;
+ string variant = 3;
}
diff --git a/core/proto/android/os/incident_proto.proto b/core/proto/android/os/incident.proto
similarity index 69%
rename from core/proto/android/os/incident_proto.proto
rename to core/proto/android/os/incident.proto
index 1708b81..ec2f32b 100644
--- a/core/proto/android/os/incident_proto.proto
+++ b/core/proto/android/os/incident.proto
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-syntax = "proto2";
+syntax = "proto3";
-option java_package = "android.os";
option java_multiple_files = true;
+option java_outer_classname = "IncidentProtoMetadata";
import "frameworks/base/libs/incident/proto/android/privacy.proto";
-import "frameworks/base/core/proto/android/service/fingerprint_proto.proto";
+import "frameworks/base/core/proto/android/service/fingerprint.proto";
package android.os;
@@ -32,7 +32,7 @@
CAUSE_CRASH = 3;
}
- optional Cause cause = 1;
+ Cause cause = 1;
}
message IncidentProto {
@@ -40,13 +40,13 @@
repeated IncidentHeaderProto header = 1;
// Device information
- //optional SystemProperties system_properties = 1000;
+ //SystemProperties system_properties = 1000;
// Linux services
- //optional Procrank procrank = 2000;
- //optional PageTypeInfo page_type_info = 2001;
- //optional KernelWakeSources kernel_wake_sources = 2002;
+ //Procrank procrank = 2000;
+ //PageTypeInfo page_type_info = 2001;
+ //KernelWakeSources kernel_wake_sources = 2002;
// System Services
- optional android.service.fingerprint.FingerprintServiceDumpProto fingerprint = 3000;
+ android.service.fingerprint.FingerprintServiceDumpProto fingerprint = 3000;
}
diff --git a/core/proto/android/service/fingerprint_proto.proto b/core/proto/android/service/fingerprint.proto
similarity index 81%
rename from core/proto/android/service/fingerprint_proto.proto
rename to core/proto/android/service/fingerprint.proto
index b2c5000..79dba86 100644
--- a/core/proto/android/service/fingerprint_proto.proto
+++ b/core/proto/android/service/fingerprint.proto
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-syntax = "proto2";
+syntax = "proto3";
package android.service.fingerprint;
option java_multiple_files = true;
+option java_outer_classname = "FingerprintServiceProto";
message FingerprintServiceDumpProto {
// Each log may include multiple tuples of (user_id, num_fingerprints).
@@ -27,30 +28,30 @@
message FingerprintUserStatsProto {
// Should be 0, 10, 11, 12, etc. where 0 is the owner.
- optional int32 user_id = 1;
+ int32 user_id = 1;
// The number of fingerprints registered to this user.
- optional int32 num_fingerprints = 2;
+ int32 num_fingerprints = 2;
// Normal fingerprint authentications (e.g. lockscreen).
- optional FingerprintActionStatsProto normal = 3;
+ FingerprintActionStatsProto normal = 3;
// Crypto authentications (e.g. to unlock password storage, make secure
// purchases, etc).
- optional FingerprintActionStatsProto crypto = 4;
+ FingerprintActionStatsProto crypto = 4;
}
message FingerprintActionStatsProto {
// Number of accepted fingerprints.
- optional int32 accept = 1;
+ int32 accept = 1;
// Number of rejected fingerprints.
- optional int32 reject = 2;
+ int32 reject = 2;
// Total number of acquisitions. Should be >= accept+reject due to poor
// image acquisition in some cases (too fast, too slow, dirty sensor, etc.)
- optional int32 acquire = 3;
+ int32 acquire = 3;
// Total number of lockouts.
- optional int32 lockout = 4;
+ int32 lockout = 4;
}
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_0_bars.xml b/core/res/res/drawable/ic_signal_wifi_badged_0_bars.xml
index 3190681..bd1eb41 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_0_bars.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_0_bars.xml
@@ -21,10 +21,9 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-377 308.5c0 -2.5 2 -4.5 4.5 -4.5l3.5 0 0.79999 -1c-0.29999 -0.29999 -3.70001 -3 -8.79999 -3 -5.09998 0 -8.5 2.79999 -8.79999 3l8.79999 11 0 0 0 0 1.60001 -2c-0.9 -0.89999 -1.60001 -2.10001 -1.60001 -3.5z"
- android:fillColor="#4285F4"
+ android:fillColor="#FFFFFF"
android:fillAlpha="0.3" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_1_bar.xml b/core/res/res/drawable/ic_signal_wifi_badged_1_bar.xml
index 202f484..aedb12c 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_1_bar.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_1_bar.xml
@@ -21,13 +21,12 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-377 308.5c0 -2.5 2 -4.5 4.5 -4.5l3.5 0 0.79999 -1c-0.29999 -0.29999 -3.70001 -3 -8.79999 -3 -5.09998 0 -8.5 2.79999 -8.79999 3l8.79999 11 0 0 0 0 1.60001 -2c-0.9 -0.89999 -1.60001 -2.10001 -1.60001 -3.5z"
- android:fillColor="#4285F4"
+ android:fillColor="#FFFFFF"
android:fillAlpha="0.3" />
<path
android:pathData="M-377 308.5c0 -0.20001 0 -0.29999 0.10001 -0.5 0 0 0 0 -0.10001 0 -2.10001 0 -3.60001 1.20001 -3.79999 1.29999L-377 314l1.60001 -2.10001c-0.9 -0.79998 -1.60001 -2 -1.60001 -3.39999z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_2_bars.xml b/core/res/res/drawable/ic_signal_wifi_badged_2_bars.xml
index 188a3a4..6f07cb5 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_2_bars.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_2_bars.xml
@@ -21,13 +21,12 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-377 308.5c0 -2.5 2 -4.5 4.5 -4.5l3.5 0 0.79999 -1c-0.29999 -0.29999 -3.70001 -3 -8.79999 -3 -5.09998 0 -8.5 2.79999 -8.79999 3l8.79999 11 0 0 0 0 1.60001 -2c-0.9 -0.89999 -1.60001 -2.10001 -1.60001 -3.5z"
- android:fillColor="#4285F4"
+ android:fillColor="#FFFFFF"
android:fillAlpha="0.3" />
<path
android:pathData="M-377 308.5c0 -0.89999 0.29999 -1.70001 0.70001 -2.5 -0.20001 0 -0.5 0 -0.70001 0 -2.79999 0 -4.79999 1.60001 -5 1.79999l5 6.20001 0 0 0 0 1.60001 -2c-1 -0.89999 -1.60001 -2.10001 -1.60001 -3.5z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_3_bars.xml b/core/res/res/drawable/ic_signal_wifi_badged_3_bars.xml
index 28a3846..c41a8ca 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_3_bars.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_3_bars.xml
@@ -21,13 +21,12 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-377 308.5c0 -2.5 2 -4.5 4.5 -4.5l3.5 0 0.79999 -1c-0.29999 -0.29999 -3.70001 -3 -8.79999 -3 -5.09998 0 -8.5 2.79999 -8.79999 3l8.79999 11 0 0 0 0 1.60001 -2c-0.9 -0.89999 -1.60001 -2.10001 -1.60001 -3.5z"
- android:fillColor="#4285F4"
+ android:fillColor="#FFFFFF"
android:fillAlpha="0.3" />
<path
android:pathData="M-375.39999 311.89999c-1 -0.79998 -1.60001 -2.1 -1.60001 -3.39999 0 -1.79999 1.10001 -3.39999 2.70001 -4.10001C-375.09998 304.19998 -376 304 -377 304c-3.60001 0 -6 1.89999 -6.29999 2.20001L-377 314l0 0 0 0"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_4_bars.xml b/core/res/res/drawable/ic_signal_wifi_badged_4_bars.xml
index 3a721eb..ec0a52f 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_4_bars.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_4_bars.xml
@@ -21,9 +21,8 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-377 308.5c0 -2.5 2 -4.5 4.5 -4.5l3.5 0 0.79999 -1c-0.29999 -0.29999 -3.70001 -3 -8.79999 -3 -5.09998 0 -8.5 2.79999 -8.79999 3l8.79999 11 0 0 0 0 1.60001 -2c-0.9 -0.89999 -1.60001 -2.10001 -1.60001 -3.5z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_4k.xml b/core/res/res/drawable/ic_signal_wifi_badged_4k.xml
index 048db3d..78bd0a0 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_4k.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_4k.xml
@@ -21,12 +21,11 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-373.04999 308.79999l0.5 0 0 0.89999 -0.5 0 0 1.20001 -1.1 0 0 -1.20001 -1.9 0 -0.1 -0.70001 1.89999 -3.70001 1.10001 0 0 3.50003 0.1 0zm-1.89999 0l0.89999 0 0 -1.9 0 0 -0.89999 1.9z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
<path
android:pathData="M-370.44998 308.70001l-0.5 0.60001 0 1.70001 -1.10001 0 0 -5.70001 1.10001 0 0 2.5 0.39999 -0.60001 1.10001 -1.89999 1.39999 0 -1.6 2.5 1.70001 3.20001 -1.29999 0 -1.20001 -2.30002z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_hd.xml b/core/res/res/drawable/ic_signal_wifi_badged_hd.xml
index 76c56ff..78085c2f 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_hd.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_hd.xml
@@ -21,12 +21,11 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-371.79999 311l-1.1 0 0 -2.29999 -0.79999 0 0 2.29999 -1.10001 0 0 -5.70001 1.10001 0 0 2.39999 0.79999 0 0 -2.39999 1.1 0 0 5.70001z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
<path
android:pathData="M-371.33557 310.98651l0 -5.68701 1.39068 0c0.27848 0 0.53336 0.0532 0.76465 0.16016 0.2313 0.10693 0.42954 0.2622 0.59568 0.46679 0.16519 0.20459 0.29357 0.45557 0.38421 0.75391 0.0906 0.29834 0.13593 0.63867 0.13593 1.02148l0 0.88672c0 0.38281 -0.0453 0.72363 -0.13593 1.021 -0.0906 0.29785 -0.21902 0.5498 -0.38421 0.7539 -0.16614 0.20411 -0.36628 0.35938 -0.59946 0.46485 -0.23316 0.10547 -0.49182 0.1582 -0.77786 0.1582l-1.37369 0zm1.06879 -4.76904l0 3.85107 0.26333 0c0.15491 0 0.28452 -0.0283 0.38971 -0.084 0.10516 -0.0557 0.19077 -0.14356 0.25681 -0.26367 0.066 -0.12012 0.11331 -0.27198 0.14184 -0.4585 0.0285 -0.18652 0.0424 -0.41113 0.0424 -0.67383l0 -0.89453c0 -0.26562 -0.0138 -0.49219 -0.0424 -0.67969 -0.0285 -0.1875 -0.0758 -0.33984 -0.14102 -0.45703 -0.0644 -0.11719 -0.1492 -0.20312 -0.25275 -0.25781 -0.10437 -0.0547 -0.23071 -0.082 -0.37991 -0.082l-0.27801 0z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_ld.xml b/core/res/res/drawable/ic_signal_wifi_badged_ld.xml
index 20f8983..f660ab7 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_ld.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_ld.xml
@@ -21,12 +21,11 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-371.33557 310.98651l0 -5.68701 1.39068 0c0.27848 0 0.53336 0.0532 0.76465 0.16016 0.2313 0.10693 0.42954 0.2622 0.59568 0.46679 0.16519 0.20459 0.29357 0.45557 0.38421 0.75391 0.0906 0.29834 0.13593 0.63867 0.13593 1.02148l0 0.88672c0 0.38281 -0.0453 0.72363 -0.13593 1.021 -0.0906 0.29785 -0.21902 0.5498 -0.38421 0.7539 -0.16614 0.20411 -0.36628 0.35938 -0.59946 0.46485 -0.23316 0.10547 -0.49182 0.1582 -0.77786 0.1582l-1.37369 0zm1.06879 -4.76904l0 3.85107 0.26333 0c0.15491 0 0.28452 -0.0283 0.38971 -0.084 0.10516 -0.0557 0.19077 -0.14356 0.25681 -0.26367 0.066 -0.12012 0.11331 -0.27198 0.14184 -0.4585 0.0285 -0.18652 0.0424 -0.41113 0.0424 -0.67383l0 -0.89453c0 -0.26562 -0.0138 -0.49219 -0.0424 -0.67969 -0.0285 -0.1875 -0.0758 -0.33984 -0.14102 -0.45703 -0.0644 -0.11719 -0.1492 -0.20312 -0.25275 -0.25781 -0.10437 -0.0547 -0.23071 -0.082 -0.37991 -0.082l-0.27801 0z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
<path
android:pathData="M-373.13333 310.13333l1.33334 0 0 0.86667 -2.46667 0 0 -5.66666 1.13333 0 0 4.79999z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/res/res/drawable/ic_signal_wifi_badged_sd.xml b/core/res/res/drawable/ic_signal_wifi_badged_sd.xml
index 4f4254b..43b8653 100644
--- a/core/res/res/drawable/ic_signal_wifi_badged_sd.xml
+++ b/core/res/res/drawable/ic_signal_wifi_badged_sd.xml
@@ -21,12 +21,11 @@
<group
android:translateX="386"
android:translateY="-298">
- <!-- TODO(b/33778046): update fillColor below to refer to a theme color attribute instead of hardcoded value. -->
<path
android:pathData="M-371.33557 310.98651l0 -5.68701 1.39068 0c0.27848 0 0.53336 0.0532 0.76465 0.16016 0.2313 0.10693 0.42954 0.2622 0.59568 0.46679 0.16519 0.20459 0.29357 0.45557 0.38421 0.75391 0.0906 0.29834 0.13593 0.63867 0.13593 1.02148l0 0.88672c0 0.38281 -0.0453 0.72363 -0.13593 1.021 -0.0906 0.29785 -0.21902 0.5498 -0.38421 0.7539 -0.16614 0.20411 -0.36628 0.35938 -0.59946 0.46485 -0.23316 0.10547 -0.49182 0.1582 -0.77786 0.1582l-1.37369 0zm1.06879 -4.76904l0 3.85107 0.26333 0c0.15491 0 0.28452 -0.0283 0.38971 -0.084 0.10516 -0.0557 0.19077 -0.14356 0.25681 -0.26367 0.066 -0.12012 0.11331 -0.27198 0.14184 -0.4585 0.0285 -0.18652 0.0424 -0.41113 0.0424 -0.67383l0 -0.89453c0 -0.26562 -0.0138 -0.49219 -0.0424 -0.67969 -0.0285 -0.1875 -0.0758 -0.33984 -0.14102 -0.45703 -0.0644 -0.11719 -0.1492 -0.20312 -0.25275 -0.25781 -0.10437 -0.0547 -0.23071 -0.082 -0.37991 -0.082l-0.27801 0z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
<path
android:pathData="M-372.87598 309.47461c0 -0.10645 -0.01 -0.20117 -0.0303 -0.28223 -0.0205 -0.0811 -0.0576 -0.15527 -0.11035 -0.22265 -0.0537 -0.0674 -0.12598 -0.12891 -0.21777 -0.18457 -0.0908 -0.0566 -0.20704 -0.11231 -0.34668 -0.16797 -0.24903 -0.0889 -0.47657 -0.18457 -0.68165 -0.28614 -0.20605 -0.10156 -0.38281 -0.2207 -0.53027 -0.35839 -0.14746 -0.13721 -0.26172 -0.29639 -0.34277 -0.47803 -0.0811 -0.18164 -0.12207 -0.39697 -0.12207 -0.646 0 -0.23144 0.042 -0.4414 0.12793 -0.63086 0.085 -0.18945 0.20312 -0.35205 0.35644 -0.48779 0.15235 -0.13623 0.33496 -0.2417 0.54883 -0.31641 0.21289 -0.0752 0.44824 -0.1123 0.70508 -0.1123 0.2666 0 0.50683 0.0425 0.71973 0.12744 0.21386 0.0854 0.39648 0.2041 0.54687 0.35645 0.15137 0.15234 0.26758 0.333 0.34766 0.54101 0.0791 0.2085 0.11914 0.43604 0.11914 0.68262l-1.07422 0c0 -0.11963 -0.0127 -0.22998 -0.0381 -0.33154 -0.0254 -0.10205 -0.0654 -0.18897 -0.12011 -0.26123 -0.0547 -0.0723 -0.125 -0.12891 -0.20997 -0.16993 -0.085 -0.0405 -0.1875 -0.0605 -0.30664 -0.0605 -0.11132 0 -0.208 0.0171 -0.29004 0.0513 -0.0811 0.0342 -0.14843 0.0815 -0.20117 0.14111 -0.0537 0.0596 -0.0928 0.13037 -0.11816 0.21143 -0.0254 0.0815 -0.0381 0.16894 -0.0381 0.26318 0 0.0937 0.0166 0.17725 0.0508 0.24951 0.0342 0.0723 0.0869 0.14014 0.15625 0.20361 0.0703 0.064 0.16016 0.125 0.26856 0.18311 0.10937 0.0586 0.23926 0.11963 0.38965 0.1831 0.24316 0.084 0.46093 0.17823 0.65136 0.2837 0.19043 0.10498 0.35059 0.22998 0.48047 0.37158 0.12891 0.14258 0.22754 0.30762 0.29493 0.49316 0.0674 0.1875 0.10156 0.40235 0.10156 0.64649 0 0.24121 -0.04 0.458 -0.12012 0.64843 -0.0801 0.19043 -0.19434 0.35059 -0.3418 0.48145 -0.14746 0.13086 -0.32617 0.23144 -0.53711 0.30176 -0.21093 0.0693 -0.44726 0.10449 -0.70898 0.10449 -0.23633 0 -0.46777 -0.0361 -0.69531 -0.1084 -0.22754 -0.0723 -0.43067 -0.18359 -0.61035 -0.33203 -0.17872 -0.14844 -0.32325 -0.33691 -0.43262 -0.56543 -0.10938 -0.22852 -0.16309 -0.49805 -0.16309 -0.80859l1.07813 0c0 0.17089 0.0166 0.31445 0.0498 0.43261 0.0332 0.11817 0.084 0.21485 0.15235 0.29004 0.0684 0.0752 0.15429 0.12891 0.25683 0.16211 0.10352 0.0332 0.22461 0.0488 0.36426 0.0488 0.11719 0 0.21582 -0.0156 0.2959 -0.0488 0.0801 -0.0332 0.14355 -0.0781 0.19238 -0.13574 0.0478 -0.0566 0.082 -0.125 0.10254 -0.2041 0.0205 -0.0781 0.0303 -0.16504 0.0303 -0.25879z"
- android:fillColor="#4285F4" />
+ android:fillColor="#FFFFFF" />
</group>
</vector>
diff --git a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
index 9a81401..bdc0200 100644
--- a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
+++ b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
@@ -77,7 +77,7 @@
final NetworkRecommendationProvider.ResultCallback callback =
new NetworkRecommendationProvider.ResultCallback(mMockRemoteCallback, sequence);
- final RecommendationResult result = new RecommendationResult(null);
+ final RecommendationResult result = RecommendationResult.createDoNotConnectRecommendation();
callback.onResult(result);
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -93,7 +93,7 @@
final NetworkRecommendationProvider.ResultCallback callback =
new NetworkRecommendationProvider.ResultCallback(mMockRemoteCallback, sequence);
- final RecommendationResult result = new RecommendationResult(null);
+ final RecommendationResult result = RecommendationResult.createDoNotConnectRecommendation();
callback.onResult(result);
try {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java
index 91ab12b..5d843c1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java
@@ -102,11 +102,11 @@
@Test
public void addIntentParameters_configFalse_argumentTrue() {
- when(mContext.getResources().getBoolean(R.bool.config_sendPackageName)).thenReturn(true);
+ when(mContext.getResources().getBoolean(R.bool.config_sendPackageName)).thenReturn(false);
Intent intent = new Intent();
HelpUtils.addIntentParameters(
- mContext, intent, null /* backupContext */, false /* sendPackageName */);
+ mContext, intent, null /* backupContext */, true /* sendPackageName */);
assertThat(intent.hasExtra(HELP_INTENT_EXTRA_KEY)).isFalse();
assertThat(intent.hasExtra(HELP_INTENT_NAME_KEY)).isFalse();
diff --git a/packages/SettingsProvider/Android.mk b/packages/SettingsProvider/Android.mk
index 2b833b2..710214c 100644
--- a/packages/SettingsProvider/Android.mk
+++ b/packages/SettingsProvider/Android.mk
@@ -3,7 +3,7 @@
LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(call all-subdir-java-files) \
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
src/com/android/providers/settings/EventLogTags.logtags
LOCAL_JAVA_LIBRARIES := telephony-common ims-common
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index c149876..3e62158 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -84,6 +84,10 @@
import java.util.Set;
import java.util.regex.Pattern;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SYSTEM_UID;
+import static android.os.Process.SHELL_UID;
+
/**
* <p>
* This class is a content provider that publishes the system settings.
@@ -147,6 +151,7 @@
private static final int MUTATION_OPERATION_INSERT = 1;
private static final int MUTATION_OPERATION_DELETE = 2;
private static final int MUTATION_OPERATION_UPDATE = 3;
+ private static final int MUTATION_OPERATION_RESET = 4;
private static final String[] ALL_COLUMNS = new String[] {
Settings.NameValueTable._ID,
@@ -292,13 +297,17 @@
case Settings.CALL_METHOD_PUT_GLOBAL: {
String value = getSettingValue(args);
- insertGlobalSetting(name, value, requestingUserId, false);
+ String tag = getSettingTag(args);
+ final boolean makeDefault = getSettingMakeDefault(args);
+ insertGlobalSetting(name, value, tag, makeDefault, requestingUserId, false);
break;
}
case Settings.CALL_METHOD_PUT_SECURE: {
String value = getSettingValue(args);
- insertSecureSetting(name, value, requestingUserId, false);
+ String tag = getSettingTag(args);
+ final boolean makeDefault = getSettingMakeDefault(args);
+ insertSecureSetting(name, value, tag, makeDefault, requestingUserId, false);
break;
}
@@ -308,6 +317,20 @@
break;
}
+ case Settings.CALL_METHOD_RESET_GLOBAL: {
+ final int mode = getResetModeEnforcingPermission(args);
+ String tag = getSettingTag(args);
+ resetGlobalSetting(requestingUserId, mode, tag);
+ break;
+ }
+
+ case Settings.CALL_METHOD_RESET_SECURE: {
+ final int mode = getResetModeEnforcingPermission(args);
+ String tag = getSettingTag(args);
+ resetSecureSetting(requestingUserId, mode, tag);
+ break;
+ }
+
default: {
Slog.w(LOG_TAG, "call() with invalid method: " + method);
} break;
@@ -399,13 +422,15 @@
switch (table) {
case TABLE_GLOBAL: {
- if (insertGlobalSetting(name, value, UserHandle.getCallingUserId(), false)) {
+ if (insertGlobalSetting(name, value, null, false,
+ UserHandle.getCallingUserId(), false)) {
return Uri.withAppendedPath(Settings.Global.CONTENT_URI, name);
}
} break;
case TABLE_SECURE: {
- if (insertSecureSetting(name, value, UserHandle.getCallingUserId(), false)) {
+ if (insertSecureSetting(name, value, null, false,
+ UserHandle.getCallingUserId(), false)) {
return Uri.withAppendedPath(Settings.Secure.CONTENT_URI, name);
}
} break;
@@ -503,12 +528,14 @@
switch (args.table) {
case TABLE_GLOBAL: {
final int userId = UserHandle.getCallingUserId();
- return updateGlobalSetting(args.name, value, userId, false) ? 1 : 0;
+ return updateGlobalSetting(args.name, value, null, false,
+ userId, false) ? 1 : 0;
}
case TABLE_SECURE: {
final int userId = UserHandle.getCallingUserId();
- return updateSecureSetting(args.name, value, userId, false) ? 1 : 0;
+ return updateSecureSetting(args.name, value, null, false,
+ userId, false) ? 1 : 0;
}
case TABLE_SYSTEM: {
@@ -586,10 +613,9 @@
SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
if (globalSettings != null) {
dumpSettingsLocked(globalSettings, pw);
+ pw.println();
+ globalSettings.dumpHistoricalOperations(pw);
}
- pw.println();
-
- globalSettings.dumpHistoricalOperations(pw);
}
pw.println("SECURE SETTINGS (user " + userId + ")");
@@ -597,20 +623,18 @@
SETTINGS_TYPE_SECURE, userId);
if (secureSettings != null) {
dumpSettingsLocked(secureSettings, pw);
+ pw.println();
+ secureSettings.dumpHistoricalOperations(pw);
}
- pw.println();
-
- secureSettings.dumpHistoricalOperations(pw);
pw.println("SYSTEM SETTINGS (user " + userId + ")");
SettingsState systemSettings = mSettingsRegistry.getSettingsLocked(
SETTINGS_TYPE_SYSTEM, userId);
if (systemSettings != null) {
dumpSettingsLocked(systemSettings, pw);
+ pw.println();
+ systemSettings.dumpHistoricalOperations(pw);
}
- pw.println();
-
- systemSettings.dumpHistoricalOperations(pw);
}
private void dumpSettingsLocked(SettingsState settingsState, PrintWriter pw) {
@@ -624,9 +648,16 @@
pw.print("_id:"); pw.print(toDumpString(setting.getId()));
pw.print(" name:"); pw.print(toDumpString(name));
if (setting.getPackageName() != null) {
- pw.print(" pkg:"); pw.print(toDumpString(setting.getPackageName()));
+ pw.print(" pkg:"); pw.print(setting.getPackageName());
}
pw.print(" value:"); pw.print(toDumpString(setting.getValue()));
+ if (setting.getDefaultValue() != null) {
+ pw.print(" default:"); pw.print(setting.getDefaultValue());
+ pw.print(" defaultSystemSet:"); pw.print(setting.isDefaultSystemSet());
+ }
+ if (setting.getTag() != null) {
+ pw.print(" tag:"); pw.print(setting.getTag());
+ }
pw.println();
}
}
@@ -691,73 +722,79 @@
// value with a forced update to ensure that all cross profile dependencies
// are taken into account. Also make sure the settings update to.. the same
// value passes the security checks, so clear binder calling id.
- if (newRestrictions.containsKey(UserManager.DISALLOW_SHARE_LOCATION)
- != prevRestrictions.containsKey(UserManager.DISALLOW_SHARE_LOCATION)) {
+ if (newRestrictions.getBoolean(UserManager.DISALLOW_SHARE_LOCATION)
+ != prevRestrictions.getBoolean(UserManager.DISALLOW_SHARE_LOCATION)) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
Setting setting = getSecureSetting(
Settings.Secure.LOCATION_PROVIDERS_ALLOWED, userId);
updateSecureSetting(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
- setting != null ? setting.getValue() : null, userId, true);
+ setting != null ? setting.getValue() : null, null,
+ true, userId, true);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
- if (newRestrictions.containsKey(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES)
- != prevRestrictions.containsKey(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES)) {
+ if (newRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES)
+ != prevRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES)) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
Setting setting = getGlobalSetting(Settings.Global.INSTALL_NON_MARKET_APPS);
+ String value = setting != null ? setting.getValue() : null;
updateGlobalSetting(Settings.Global.INSTALL_NON_MARKET_APPS,
- setting != null ? setting.getValue() : null, userId, true);
+ value, null, true, userId, true);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
- if (newRestrictions.containsKey(UserManager.DISALLOW_DEBUGGING_FEATURES)
- != prevRestrictions.containsKey(UserManager.DISALLOW_DEBUGGING_FEATURES)) {
+ if (newRestrictions.getBoolean(UserManager.DISALLOW_DEBUGGING_FEATURES)
+ != prevRestrictions.getBoolean(UserManager.DISALLOW_DEBUGGING_FEATURES)) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
Setting setting = getGlobalSetting(Settings.Global.ADB_ENABLED);
+ String value = setting != null ? setting.getValue() : null;
updateGlobalSetting(Settings.Global.ADB_ENABLED,
- setting != null ? setting.getValue() : null, userId, true);
+ value, null, true, userId, true);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
- if (newRestrictions.containsKey(UserManager.ENSURE_VERIFY_APPS)
- != prevRestrictions.containsKey(UserManager.ENSURE_VERIFY_APPS)) {
+ if (newRestrictions.getBoolean(UserManager.ENSURE_VERIFY_APPS)
+ != prevRestrictions.getBoolean(UserManager.ENSURE_VERIFY_APPS)) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
Setting enable = getGlobalSetting(
Settings.Global.PACKAGE_VERIFIER_ENABLE);
+ String enableValue = enable != null ? enable.getValue() : null;
updateGlobalSetting(Settings.Global.PACKAGE_VERIFIER_ENABLE,
- enable != null ? enable.getValue() : null, userId, true);
+ enableValue, null, true, userId, true);
Setting include = getGlobalSetting(
Settings.Global.PACKAGE_VERIFIER_INCLUDE_ADB);
+ String includeValue = include != null ? include.getValue() : null;
updateGlobalSetting(Settings.Global.PACKAGE_VERIFIER_INCLUDE_ADB,
- include != null ? include.getValue() : null, userId, true);
+ includeValue, null, true, userId, true);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
- if (newRestrictions.containsKey(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)
- != prevRestrictions.containsKey(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) {
+ if (newRestrictions.getBoolean(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)
+ != prevRestrictions.getBoolean(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
Setting setting = getGlobalSetting(
Settings.Global.PREFERRED_NETWORK_MODE);
+ String value = setting != null ? setting.getValue() : null;
updateGlobalSetting(Settings.Global.PREFERRED_NETWORK_MODE,
- setting != null ? setting.getValue() : null, userId, true);
+ value, null, true, userId, true);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -806,34 +843,49 @@
}
}
- private boolean updateGlobalSetting(String name, String value, int requestingUserId,
- boolean forceNotify) {
+ private boolean updateGlobalSetting(String name, String value, String tag,
+ boolean makeDefault, int requestingUserId, boolean forceNotify) {
if (DEBUG) {
- Slog.v(LOG_TAG, "updateGlobalSetting(" + name + ", " + value + ")");
+ Slog.v(LOG_TAG, "updateGlobalSetting(" + name + ", " + value + ", "
+ + ", " + tag + ", " + makeDefault + ", " + requestingUserId
+ + ", " + forceNotify + ")");
}
- return mutateGlobalSetting(name, value, requestingUserId, MUTATION_OPERATION_UPDATE,
- forceNotify);
+ return mutateGlobalSetting(name, value, tag, makeDefault, requestingUserId,
+ MUTATION_OPERATION_UPDATE, forceNotify, 0);
}
- private boolean insertGlobalSetting(String name, String value, int requestingUserId,
- boolean forceNotify) {
+ private boolean insertGlobalSetting(String name, String value, String tag,
+ boolean makeDefault, int requestingUserId, boolean forceNotify) {
if (DEBUG) {
- Slog.v(LOG_TAG, "insertGlobalSetting(" + name + ", " + value + ")");
+ Slog.v(LOG_TAG, "insertGlobalSetting(" + name + ", " + value + ", "
+ + ", " + tag + ", " + makeDefault + ", " + requestingUserId
+ + ", " + forceNotify + ")");
}
- return mutateGlobalSetting(name, value, requestingUserId, MUTATION_OPERATION_INSERT,
- forceNotify);
+ return mutateGlobalSetting(name, value, tag, makeDefault, requestingUserId,
+ MUTATION_OPERATION_INSERT, forceNotify, 0);
}
private boolean deleteGlobalSetting(String name, int requestingUserId, boolean forceNotify) {
if (DEBUG) {
- Slog.v(LOG_TAG, "deleteGlobalSettingLocked(" + name + ")");
+ Slog.v(LOG_TAG, "deleteGlobalSetting(" + name + ", " + requestingUserId
+ + ", " + forceNotify + ")");
}
- return mutateGlobalSetting(name, null, requestingUserId, MUTATION_OPERATION_DELETE,
- forceNotify);
+ return mutateGlobalSetting(name, null, null, false, requestingUserId,
+ MUTATION_OPERATION_DELETE, forceNotify, 0);
}
- private boolean mutateGlobalSetting(String name, String value, int requestingUserId,
- int operation, boolean forceNotify) {
+ private void resetGlobalSetting(int requestingUserId, int mode, String tag) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "resetGlobalSetting(" + requestingUserId + ", "
+ + mode + ", " + tag + ")");
+ }
+ mutateGlobalSetting(null, null, tag, false, requestingUserId,
+ MUTATION_OPERATION_RESET, false, mode);
+ }
+
+ private boolean mutateGlobalSetting(String name, String value, String tag,
+ boolean makeDefault, int requestingUserId, int operation, boolean forceNotify,
+ int mode) {
// Make sure the caller can change the settings - treated as secure.
enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
@@ -842,7 +894,7 @@
// If this is a setting that is currently restricted for this user, do not allow
// unrestricting changes.
- if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId, value,
+ if (name != null && isGlobalOrSecureSettingRestrictedForUser(name, callingUserId, value,
Binder.getCallingUid())) {
return false;
}
@@ -851,9 +903,9 @@
synchronized (mLock) {
switch (operation) {
case MUTATION_OPERATION_INSERT: {
- return mSettingsRegistry
- .insertSettingLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM,
- name, value, getCallingPackage(), forceNotify);
+ return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_SYSTEM, name, value, tag, makeDefault,
+ getCallingPackage(), forceNotify);
}
case MUTATION_OPERATION_DELETE: {
@@ -862,10 +914,15 @@
}
case MUTATION_OPERATION_UPDATE: {
- return mSettingsRegistry
- .updateSettingLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM,
- name, value, getCallingPackage(), forceNotify);
+ return mSettingsRegistry.updateSettingLocked(SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_SYSTEM, name, value, tag, makeDefault,
+ getCallingPackage(), forceNotify);
}
+
+ case MUTATION_OPERATION_RESET: {
+ mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_GLOBAL,
+ UserHandle.USER_SYSTEM, getCallingPackage(), mode, tag);
+ } return true;
}
}
@@ -934,39 +991,53 @@
}
}
- private boolean insertSecureSetting(String name, String value, int requestingUserId,
- boolean forceNotify) {
+ private boolean insertSecureSetting(String name, String value, String tag,
+ boolean makeDefault, int requestingUserId, boolean forceNotify) {
if (DEBUG) {
Slog.v(LOG_TAG, "insertSecureSetting(" + name + ", " + value + ", "
- + requestingUserId + ")");
+ + ", " + tag + ", " + makeDefault + ", " + requestingUserId
+ + ", " + forceNotify + ")");
}
- return mutateSecureSetting(name, value, requestingUserId, MUTATION_OPERATION_INSERT,
- forceNotify);
+ return mutateSecureSetting(name, value, tag, makeDefault, requestingUserId,
+ MUTATION_OPERATION_INSERT, forceNotify, 0);
}
private boolean deleteSecureSetting(String name, int requestingUserId, boolean forceNotify) {
if (DEBUG) {
- Slog.v(LOG_TAG, "deleteSecureSetting(" + name + ", " + requestingUserId + ")");
+ Slog.v(LOG_TAG, "deleteSecureSetting(" + name + ", " + requestingUserId
+ + ", " + forceNotify + ")");
}
- return mutateSecureSetting(name, null, requestingUserId, MUTATION_OPERATION_DELETE,
- forceNotify);
+ return mutateSecureSetting(name, null, null, false, requestingUserId,
+ MUTATION_OPERATION_DELETE, forceNotify, 0);
}
- private boolean updateSecureSetting(String name, String value, int requestingUserId,
- boolean forceNotify) {
+ private boolean updateSecureSetting(String name, String value, String tag,
+ boolean makeDefault, int requestingUserId, boolean forceNotify) {
if (DEBUG) {
Slog.v(LOG_TAG, "updateSecureSetting(" + name + ", " + value + ", "
- + requestingUserId + ")");
+ + ", " + tag + ", " + makeDefault + ", " + requestingUserId
+ + ", " + forceNotify +")");
}
- return mutateSecureSetting(name, value, requestingUserId, MUTATION_OPERATION_UPDATE,
- forceNotify);
+ return mutateSecureSetting(name, value, tag, makeDefault, requestingUserId,
+ MUTATION_OPERATION_UPDATE, forceNotify, 0);
}
- private boolean mutateSecureSetting(String name, String value, int requestingUserId,
- int operation, boolean forceNotify) {
+ private void resetSecureSetting(int requestingUserId, int mode, String tag) {
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "resetSecureSetting(" + requestingUserId + ", "
+ + mode + ", " + tag + ")");
+ }
+
+ mutateSecureSetting(null, null, tag, false, requestingUserId,
+ MUTATION_OPERATION_RESET, false, mode);
+ }
+
+ private boolean mutateSecureSetting(String name, String value, String tag,
+ boolean makeDefault, int requestingUserId, int operation, boolean forceNotify,
+ int mode) {
// Make sure the caller can change the settings.
enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
@@ -975,7 +1046,7 @@
// If this is a setting that is currently restricted for this user, do not allow
// unrestricting changes.
- if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId, value,
+ if (name != null && isGlobalOrSecureSettingRestrictedForUser(name, callingUserId, value,
Binder.getCallingUid())) {
return false;
}
@@ -990,7 +1061,8 @@
// Special cases for location providers (sigh).
if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
- return updateLocationProvidersAllowedLocked(value, owningUserId, forceNotify);
+ return updateLocationProvidersAllowedLocked(value, tag, owningUserId, makeDefault,
+ forceNotify);
}
// Mutate the value.
@@ -998,7 +1070,8 @@
switch (operation) {
case MUTATION_OPERATION_INSERT: {
return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_SECURE,
- owningUserId, name, value, getCallingPackage(), forceNotify);
+ owningUserId, name, value, tag, makeDefault,
+ getCallingPackage(), forceNotify);
}
case MUTATION_OPERATION_DELETE: {
@@ -1008,8 +1081,14 @@
case MUTATION_OPERATION_UPDATE: {
return mSettingsRegistry.updateSettingLocked(SETTINGS_TYPE_SECURE,
- owningUserId, name, value, getCallingPackage(), forceNotify);
+ owningUserId, name, value, tag, makeDefault,
+ getCallingPackage(), forceNotify);
}
+
+ case MUTATION_OPERATION_RESET: {
+ mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_SECURE,
+ UserHandle.USER_SYSTEM, getCallingPackage(), mode, tag);
+ } return true;
}
}
@@ -1138,7 +1217,7 @@
case MUTATION_OPERATION_INSERT: {
validateSystemSettingValue(name, value);
return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_SYSTEM,
- owningUserId, name, value, getCallingPackage(), false);
+ owningUserId, name, value, null, false, getCallingPackage(), false);
}
case MUTATION_OPERATION_DELETE: {
@@ -1149,7 +1228,7 @@
case MUTATION_OPERATION_UPDATE: {
validateSystemSettingValue(name, value);
return mSettingsRegistry.updateSettingLocked(SETTINGS_TYPE_SYSTEM,
- owningUserId, name, value, getCallingPackage(), false);
+ owningUserId, name, value, null, false, getCallingPackage(), false);
}
}
@@ -1240,7 +1319,8 @@
case Settings.Secure.ALWAYS_ON_VPN_APP:
case Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN:
// Whitelist system uid (ConnectivityService) and root uid to change always-on vpn
- if (callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID) {
+ final int appId = UserHandle.getAppId(callingUid);
+ if (appId == Process.SYSTEM_UID || appId == Process.ROOT_UID) {
return false;
}
restriction = UserManager.DISALLOW_CONFIG_VPN;
@@ -1294,9 +1374,10 @@
String name, int userId) {
// System/root/shell can mutate whatever secure settings they want.
final int callingUid = Binder.getCallingUid();
- if (callingUid == android.os.Process.SYSTEM_UID
- || callingUid == Process.SHELL_UID
- || callingUid == Process.ROOT_UID) {
+ final int appId = UserHandle.getAppId(callingUid);
+ if (appId == android.os.Process.SYSTEM_UID
+ || appId == Process.SHELL_UID
+ || appId == Process.ROOT_UID) {
return;
}
@@ -1392,8 +1473,8 @@
*
* @returns whether the enabled location providers changed.
*/
- private boolean updateLocationProvidersAllowedLocked(String value, int owningUserId,
- boolean forceNotify) {
+ private boolean updateLocationProvidersAllowedLocked(String value, String tag,
+ int owningUserId, boolean makeDefault, boolean forceNotify) {
if (TextUtils.isEmpty(value)) {
return false;
}
@@ -1466,7 +1547,7 @@
return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_SECURE,
owningUserId, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, newProviders,
- getCallingPackage(), forceNotify);
+ tag, makeDefault, getCallingPackage(), forceNotify);
}
private static void warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
@@ -1509,8 +1590,8 @@
}
Bundle result = new Bundle();
result.putString(Settings.NameValueTable.VALUE,
- setting != null && !setting.isNull() ? setting.getValue() : null);
- mSettingsRegistry.mGenerationRegistry.addGenerationData(result, setting.getkey());
+ !setting.isNull() ? setting.getValue() : null);
+ mSettingsRegistry.mGenerationRegistry.addGenerationData(result, setting.getKey());
return result;
}
@@ -1528,6 +1609,51 @@
return (args != null) ? args.getString(Settings.NameValueTable.VALUE) : null;
}
+ private static String getSettingTag(Bundle args) {
+ return (args != null) ? args.getString(Settings.CALL_METHOD_TAG_KEY) : null;
+ }
+
+ private static boolean getSettingMakeDefault(Bundle args) {
+ return (args != null) && args.getBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY);
+ }
+
+ private static int getResetModeEnforcingPermission(Bundle args) {
+ final int mode = (args != null) ? args.getInt(Settings.CALL_METHOD_RESET_MODE_KEY) : 0;
+ switch (mode) {
+ case Settings.RESET_MODE_UNTRUSTED_DEFAULTS: {
+ if (!isCallerSystemOrShellOrRootOnDebuggableBuild()) {
+ throw new SecurityException("Only system, shell/root on a "
+ + "debuggable build can reset to untrusted defaults");
+ }
+ return mode;
+ }
+ case Settings.RESET_MODE_UNTRUSTED_CHANGES: {
+ if (!isCallerSystemOrShellOrRootOnDebuggableBuild()) {
+ throw new SecurityException("Only system, shell/root on a "
+ + "debuggable build can reset untrusted changes");
+ }
+ return mode;
+ }
+ case Settings.RESET_MODE_TRUSTED_DEFAULTS: {
+ if (!isCallerSystemOrShellOrRootOnDebuggableBuild()) {
+ throw new SecurityException("Only system, shell/root on a "
+ + "debuggable build can reset to trusted defaults");
+ }
+ return mode;
+ }
+ case Settings.RESET_MODE_PACKAGE_DEFAULTS: {
+ return mode;
+ }
+ }
+ throw new IllegalArgumentException("Invalid reset mode: " + mode);
+ }
+
+ private static boolean isCallerSystemOrShellOrRootOnDebuggableBuild() {
+ final int appId = UserHandle.getAppId(Binder.getCallingUid());
+ return appId == SYSTEM_UID || (Build.IS_DEBUGGABLE
+ && (appId == SHELL_UID || appId == ROOT_UID));
+ }
+
private static String getValidTableOrThrow(Uri uri) {
if (uri.getPathSegments().size() > 0) {
String table = uri.getPathSegments().get(0);
@@ -1767,8 +1893,8 @@
private void ensureSettingsStateLocked(int key) {
if (mSettingsStates.get(key) == null) {
final int maxBytesPerPackage = getMaxBytesPerPackageForType(getTypeFromKey(key));
- SettingsState settingsState = new SettingsState(mLock, getSettingsFile(key), key,
- maxBytesPerPackage, mHandlerThread.getLooper());
+ SettingsState settingsState = new SettingsState(getContext(), mLock,
+ getSettingsFile(key), key, maxBytesPerPackage, mHandlerThread.getLooper());
mSettingsStates.put(key, settingsState);
}
}
@@ -1815,12 +1941,15 @@
}
public boolean insertSettingLocked(int type, int userId, String name, String value,
- String packageName, boolean forceNotify) {
+ String tag, boolean makeDefault, String packageName, boolean forceNotify) {
final int key = makeKey(type, userId);
+ boolean success = false;
SettingsState settingsState = peekSettingsStateLocked(key);
- final boolean success = settingsState != null
- && settingsState.insertSettingLocked(name, value, packageName);
+ if (settingsState != null) {
+ success = settingsState.insertSettingLocked(name, value,
+ tag, makeDefault, packageName);
+ }
if (forceNotify || success) {
notifyForSettingsChange(key, name);
@@ -1831,11 +1960,11 @@
public boolean deleteSettingLocked(int type, int userId, String name, boolean forceNotify) {
final int key = makeKey(type, userId);
+ boolean success = false;
SettingsState settingsState = peekSettingsStateLocked(key);
- if (settingsState == null) {
- return false;
+ if (settingsState != null) {
+ success = settingsState.deleteSettingLocked(name);
}
- final boolean success = settingsState.deleteSettingLocked(name);
if (forceNotify || success) {
notifyForSettingsChange(key, name);
@@ -1854,12 +1983,15 @@
}
public boolean updateSettingLocked(int type, int userId, String name, String value,
- String packageName, boolean forceNotify) {
+ String tag, boolean makeDefault, String packageName, boolean forceNotify) {
final int key = makeKey(type, userId);
+ boolean success = false;
SettingsState settingsState = peekSettingsStateLocked(key);
- final boolean success = settingsState != null
- && settingsState.updateSettingLocked(name, value, packageName);
+ if (settingsState != null) {
+ success = settingsState.updateSettingLocked(name, value, tag,
+ makeDefault, packageName);
+ }
if (forceNotify || success) {
notifyForSettingsChange(key, name);
@@ -1868,6 +2000,72 @@
return success;
}
+ public void resetSettingsLocked(int type, int userId, String packageName, int mode,
+ String tag) {
+ final int key = makeKey(type, userId);
+ SettingsState settingsState = peekSettingsStateLocked(key);
+ if (settingsState == null) {
+ return;
+ }
+
+ switch (mode) {
+ case Settings.RESET_MODE_PACKAGE_DEFAULTS: {
+ for (String name : settingsState.getSettingNamesLocked()) {
+ Setting setting = settingsState.getSettingLocked(name);
+ if (packageName.equals(setting.getPackageName())) {
+ if (tag != null && !tag.equals(setting.getTag())) {
+ continue;
+ }
+ if (settingsState.resetSettingLocked(name, packageName)) {
+ notifyForSettingsChange(key, name);
+ }
+ }
+ }
+ } break;
+
+ case Settings.RESET_MODE_UNTRUSTED_DEFAULTS: {
+ for (String name : settingsState.getSettingNamesLocked()) {
+ Setting setting = settingsState.getSettingLocked(name);
+ if (!SettingsState.isSystemPackage(getContext(),
+ setting.getPackageName())) {
+ if (settingsState.resetSettingLocked(name, packageName)) {
+ notifyForSettingsChange(key, name);
+ }
+ }
+ }
+ } break;
+
+ case Settings.RESET_MODE_UNTRUSTED_CHANGES: {
+ for (String name : settingsState.getSettingNamesLocked()) {
+ Setting setting = settingsState.getSettingLocked(name);
+ if (!SettingsState.isSystemPackage(getContext(),
+ setting.getPackageName())) {
+ if (setting.isDefaultSystemSet()) {
+ if (settingsState.resetSettingLocked(name, packageName)) {
+ notifyForSettingsChange(key, name);
+ }
+ } else if (settingsState.deleteSettingLocked(name)) {
+ notifyForSettingsChange(key, name);
+ }
+ }
+ }
+ } break;
+
+ case Settings.RESET_MODE_TRUSTED_DEFAULTS: {
+ for (String name : settingsState.getSettingNamesLocked()) {
+ Setting setting = settingsState.getSettingLocked(name);
+ if (setting.isDefaultSystemSet()) {
+ if (settingsState.resetSettingLocked(name, packageName)) {
+ notifyForSettingsChange(key, name);
+ }
+ } else if (settingsState.deleteSettingLocked(name)) {
+ notifyForSettingsChange(key, name);
+ }
+ }
+ } break;
+ }
+ }
+
public void onPackageRemovedLocked(String packageName, int userId) {
// Global and secure settings are signature protected. Apps signed
// by the platform certificate are generally not uninstalled and
@@ -2005,7 +2203,7 @@
while (!cursor.isAfterLast()) {
String name = cursor.getString(nameColumnIdx);
String value = cursor.getString(valueColumnIdx);
- settingsState.insertSettingLocked(name, value,
+ settingsState.insertSettingLocked(name, value, null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
cursor.moveToNext();
}
@@ -2037,7 +2235,7 @@
String androidId = Long.toHexString(new SecureRandom().nextLong());
secureSettings.insertSettingLocked(Settings.Secure.ANDROID_ID, androidId,
- SettingsState.SYSTEM_PACKAGE_NAME);
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
Slog.d(LOG_TAG, "Generated and saved new ANDROID_ID [" + androidId
+ "] for user " + userId);
@@ -2221,7 +2419,8 @@
String reason = "Settings rebuilt! Current version: "
+ curVersion + " while expected: " + newVersion;
getGlobalSettingsLocked().insertSettingLocked(
- Settings.Global.DATABASE_DOWNGRADE_REASON, reason, "android");
+ Settings.Global.DATABASE_DOWNGRADE_REASON,
+ reason, null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
// Set the global settings version if owner.
@@ -2290,11 +2489,11 @@
if (userId == UserHandle.USER_SYSTEM) {
final SettingsState globalSettings = getGlobalSettingsLocked();
globalSettings.updateSettingLocked(Settings.Global.ZEN_MODE,
- Integer.toString(Settings.Global.ZEN_MODE_OFF),
- SettingsState.SYSTEM_PACKAGE_NAME);
+ Integer.toString(Settings.Global.ZEN_MODE_OFF), null,
+ true, SettingsState.SYSTEM_PACKAGE_NAME);
globalSettings.updateSettingLocked(Settings.Global.MODE_RINGER,
- Integer.toString(AudioManager.RINGER_MODE_NORMAL),
- SettingsState.SYSTEM_PACKAGE_NAME);
+ Integer.toString(AudioManager.RINGER_MODE_NORMAL), null,
+ true, SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 119;
}
@@ -2304,7 +2503,7 @@
SettingsState secureSettings = getSecureSettingsLocked(userId);
secureSettings.insertSettingLocked(Settings.Secure.DOUBLE_TAP_TO_WAKE,
getContext().getResources().getBoolean(
- R.bool.def_double_tap_to_wake) ? "1" : "0",
+ R.bool.def_double_tap_to_wake) ? "1" : "0", null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
currentVersion = 120;
@@ -2329,8 +2528,7 @@
currentSetting.isNull()) {
secureSettings.insertSettingLocked(
Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
- defaultComponent,
- SettingsState.SYSTEM_PACKAGE_NAME);
+ defaultComponent, null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 122;
}
@@ -2347,7 +2545,7 @@
Settings.Global.ADD_USERS_WHEN_LOCKED,
getContext().getResources().getBoolean(
R.bool.def_add_users_from_lockscreen) ? "1" : "0",
- SettingsState.SYSTEM_PACKAGE_NAME);
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
}
currentVersion = 123;
@@ -2358,7 +2556,7 @@
String defaultDisabledProfiles = (getContext().getResources().getString(
R.string.def_bluetooth_disabled_profiles));
globalSettings.insertSettingLocked(Settings.Global.BLUETOOTH_DISABLED_PROFILES,
- defaultDisabledProfiles, SettingsState.SYSTEM_PACKAGE_NAME);
+ defaultDisabledProfiles, null, true, SettingsState.SYSTEM_PACKAGE_NAME);
currentVersion = 124;
}
@@ -2373,7 +2571,7 @@
Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
getContext().getResources().getBoolean(
R.bool.def_show_ime_with_hard_keyboard) ? "1" : "0",
- SettingsState.SYSTEM_PACKAGE_NAME);
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 125;
}
@@ -2400,7 +2598,7 @@
}
secureSettings.insertSettingLocked(
Settings.Secure.ENABLED_VR_LISTENERS, b.toString(),
- SettingsState.SYSTEM_PACKAGE_NAME);
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
}
@@ -2420,7 +2618,7 @@
final SettingsState secureSettings = getSecureSettingsLocked(userId);
secureSettings.insertSettingLocked(
Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
- showNotifications.getValue(),
+ showNotifications.getValue(), null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
}
@@ -2430,7 +2628,7 @@
final SettingsState secureSettings = getSecureSettingsLocked(userId);
secureSettings.insertSettingLocked(
Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS,
- allowPrivate.getValue(),
+ allowPrivate.getValue(), null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
}
}
@@ -2456,7 +2654,7 @@
if (policyAccess.isNull()) {
systemSecureSettings.insertSettingLocked(
Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES,
- defaultPolicyAccess,
+ defaultPolicyAccess, null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
} else {
StringBuilder currentSetting =
@@ -2465,7 +2663,7 @@
currentSetting.append(defaultPolicyAccess);
systemSecureSettings.updateSettingLocked(
Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES,
- currentSetting.toString(),
+ currentSetting.toString(), null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
}
}
@@ -2485,7 +2683,7 @@
Settings.Secure.LONG_PRESS_TIMEOUT,
String.valueOf(getContext().getResources().getInteger(
R.integer.def_long_press_timeout_millis)),
- SettingsState.SYSTEM_PACKAGE_NAME);
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 130;
}
@@ -2498,9 +2696,9 @@
if (dozeExplicitlyDisabled) {
secureSettings.insertSettingLocked(Settings.Secure.DOZE_PULSE_ON_PICK_UP,
- "0", SettingsState.SYSTEM_PACKAGE_NAME);
+ "0", null, true, SettingsState.SYSTEM_PACKAGE_NAME);
secureSettings.insertSettingLocked(Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP,
- "0", SettingsState.SYSTEM_PACKAGE_NAME);
+ "0", null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 131;
}
@@ -2515,7 +2713,7 @@
Settings.Secure.MULTI_PRESS_TIMEOUT,
String.valueOf(getContext().getResources().getInteger(
R.integer.def_multi_press_timeout_millis)),
- SettingsState.SYSTEM_PACKAGE_NAME);
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 132;
@@ -2527,9 +2725,8 @@
String defaultSyncParentSounds = (getContext().getResources()
.getBoolean(R.bool.def_sync_parent_sounds) ? "1" : "0");
systemSecureSettings.insertSettingLocked(
- Settings.Secure.SYNC_PARENT_SOUNDS,
- defaultSyncParentSounds,
- SettingsState.SYSTEM_PACKAGE_NAME);
+ Settings.Secure.SYNC_PARENT_SOUNDS, defaultSyncParentSounds,
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
currentVersion = 133;
}
@@ -2541,7 +2738,8 @@
String defaultEndButtonBehavior = Integer.toString(getContext()
.getResources().getInteger(R.integer.def_end_button_behavior));
systemSettings.insertSettingLocked(Settings.System.END_BUTTON_BEHAVIOR,
- defaultEndButtonBehavior, SettingsState.SYSTEM_PACKAGE_NAME);
+ defaultEndButtonBehavior, null, true,
+ SettingsState.SYSTEM_PACKAGE_NAME);
}
currentVersion = 134;
}
@@ -2565,13 +2763,13 @@
// A scorer was set so enable recommendations.
globalSettings.insertSettingLocked(
Global.NETWORK_RECOMMENDATIONS_ENABLED,
- "1",
+ "1", null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
// and clear the scorer setting since it's no longer needed.
globalSettings.insertSettingLocked(
Global.NETWORK_SCORER_APP,
- null,
+ null, null, true,
SettingsState.SYSTEM_PACKAGE_NAME);
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java
index cbeb878..0808051 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java
@@ -97,6 +97,7 @@
PUT,
DELETE,
LIST,
+ RESET,
}
int mUser = -1; // unspecified
@@ -104,7 +105,10 @@
String mTable = null;
String mKey = null;
String mValue = null;
-
+ String mPackageName = null;
+ String mToken = null;
+ int mResetMode = -1;
+ boolean mMakeDefault;
MyShellCommand(SettingsProvider provider, boolean dumping) {
mProvider = provider;
@@ -142,6 +146,8 @@
mVerb = CommandVerb.DELETE;
} else if ("list".equalsIgnoreCase(arg)) {
mVerb = CommandVerb.LIST;
+ } else if ("reset".equalsIgnoreCase(arg)) {
+ mVerb = CommandVerb.RESET;
} else {
// invalid
perr.println("Invalid command: " + arg);
@@ -159,6 +165,35 @@
valid = true;
break;
}
+ } else if (mVerb == CommandVerb.RESET) {
+ if ("untrusted_defaults".equalsIgnoreCase(arg)) {
+ mResetMode = Settings.RESET_MODE_UNTRUSTED_DEFAULTS;
+ } else if ("untrusted_clear".equalsIgnoreCase(arg)) {
+ mResetMode = Settings.RESET_MODE_UNTRUSTED_CHANGES;
+ } else if ("trusted_defaults".equalsIgnoreCase(arg)) {
+ mResetMode = Settings.RESET_MODE_TRUSTED_DEFAULTS;
+ } else {
+ mPackageName = arg;
+ mResetMode = Settings.RESET_MODE_PACKAGE_DEFAULTS;
+ if (peekNextArg() == null) {
+ valid = true;
+ } else {
+ mToken = getNextArg();
+ if (peekNextArg() == null) {
+ valid = true;
+ } else {
+ perr.println("Too many arguments");
+ return -1;
+ }
+ }
+ break;
+ }
+ if (peekNextArg() == null) {
+ valid = true;
+ } else {
+ perr.println("Too many arguments");
+ return -1;
+ }
} else if (mVerb == CommandVerb.GET || mVerb == CommandVerb.DELETE) {
mKey = arg;
if (peekNextArg() == null) {
@@ -171,8 +206,32 @@
} else if (mKey == null) {
mKey = arg;
// keep going; there's another PUT arg
- } else { // PUT, final arg
+ } else if (mValue == null) {
mValue = arg;
+ // keep going; there may be another PUT arg
+ } else if (mToken == null) {
+ mToken = arg;
+ if ("default".equalsIgnoreCase(mToken)) {
+ mToken = null;
+ mMakeDefault = true;
+ if (peekNextArg() == null) {
+ valid = true;
+ } else {
+ perr.println("Too many arguments");
+ return -1;
+ }
+ break;
+ }
+ if (peekNextArg() == null) {
+ valid = true;
+ break;
+ }
+ } else { // PUT, final arg
+ if (!"default".equalsIgnoreCase(arg)) {
+ perr.println("Argument expected to be 'default'");
+ return -1;
+ }
+ mMakeDefault = true;
if (peekNextArg() == null) {
valid = true;
} else {
@@ -214,7 +273,7 @@
pout.println(getForUser(iprovider, mUser, mTable, mKey));
break;
case PUT:
- putForUser(iprovider, mUser, mTable, mKey, mValue);
+ putForUser(iprovider, mUser, mTable, mKey, mValue, mToken, mMakeDefault);
break;
case DELETE:
pout.println("Deleted "
@@ -225,6 +284,9 @@
pout.println(line);
}
break;
+ case RESET:
+ resetForUser(iprovider, mUser, mTable, mToken);
+ break;
default:
perr.println("Unspecified command");
return -1;
@@ -286,11 +348,15 @@
return result;
}
- void putForUser(IContentProvider provider, int userHandle,
- final String table, final String key, final String value) {
+ void putForUser(IContentProvider provider, int userHandle, final String table,
+ final String key, final String value, String token, boolean makeDefault) {
final String callPutCommand;
- if ("system".equals(table)) callPutCommand = Settings.CALL_METHOD_PUT_SYSTEM;
- else if ("secure".equals(table)) callPutCommand = Settings.CALL_METHOD_PUT_SECURE;
+ if ("system".equals(table)) {
+ callPutCommand = Settings.CALL_METHOD_PUT_SYSTEM;
+ makeDefault = false;
+ getOutPrintWriter().println("Ignored makeDefault - "
+ + "doesn't apply to system settings");
+ } else if ("secure".equals(table)) callPutCommand = Settings.CALL_METHOD_PUT_SECURE;
else if ("global".equals(table)) callPutCommand = Settings.CALL_METHOD_PUT_GLOBAL;
else {
getErrPrintWriter().println("Invalid table; no put performed");
@@ -301,6 +367,10 @@
Bundle arg = new Bundle();
arg.putString(Settings.NameValueTable.VALUE, value);
arg.putInt(Settings.CALL_METHOD_USER_KEY, userHandle);
+ arg.putString(Settings.CALL_METHOD_TAG_KEY, token);
+ if (makeDefault) {
+ arg.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true);
+ }
provider.call(resolveCallingPackage(), callPutCommand, key, arg);
} catch (RemoteException e) {
throw new RuntimeException("Failed in IPC", e);
@@ -327,6 +397,29 @@
return num;
}
+ void resetForUser(IContentProvider provider, int userHandle,
+ String table, String token) {
+ final String callResetCommand;
+ if ("secure".equals(table)) callResetCommand = Settings.CALL_METHOD_RESET_SECURE;
+ else if ("global".equals(table)) callResetCommand = Settings.CALL_METHOD_RESET_GLOBAL;
+ else {
+ getErrPrintWriter().println("Invalid table; no reset performed");
+ return;
+ }
+
+ try {
+ Bundle arg = new Bundle();
+ arg.putInt(Settings.CALL_METHOD_USER_KEY, userHandle);
+ arg.putInt(Settings.CALL_METHOD_RESET_MODE_KEY, mResetMode);
+ arg.putString(Settings.CALL_METHOD_TAG_KEY, token);
+ String packageName = mPackageName != null ? mPackageName : resolveCallingPackage();
+ arg.putInt(Settings.CALL_METHOD_USER_KEY, userHandle);
+ provider.call(packageName, callResetCommand, null, arg);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed in IPC", e);
+ }
+ }
+
public static String resolveCallingPackage() {
switch (Binder.getCallingUid()) {
case Process.ROOT_UID: {
@@ -360,14 +453,18 @@
pw.println(" Print this help text.");
pw.println(" get [--user <USER_ID> | current] NAMESPACE KEY");
pw.println(" Retrieve the current value of KEY.");
- pw.println(" put [--user <USER_ID> | current] NAMESPACE KEY VALUE");
+ pw.println(" put [--user <USER_ID> | current] NAMESPACE KEY VALUE [TOKEN] [default]");
pw.println(" Change the contents of KEY to VALUE.");
+ pw.println(" TOKEN to associate with the setting.");
+ pw.println(" {default} to set as the default, case-insensitive only for global/secure namespace");
pw.println(" delete NAMESPACE KEY");
pw.println(" Delete the entry for KEY.");
+ pw.println(" reset [--user <USER_ID> | current] NAMESPACE {PACKAGE_NAME | RESET_MODE}");
+ pw.println(" Reset the global/secure table for a package with mode.");
+ pw.println(" RESET_MODE is one of {untrusted_defaults, untrusted_clear, trusted_defaults}, case-insensitive");
pw.println(" list NAMESPACE");
pw.println(" Print all defined keys.");
- pw.println();
- pw.println(" NAMESPACE is one of {system, secure, global}, case-insensitive");
+ pw.println(" NAMESPACE is one of {system, secure, global}, case-insensitive");
}
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index d682fe9..4367df8 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -16,20 +16,29 @@
package com.android.providers.settings;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Base64;
import android.util.Slog;
+import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
import libcore.io.IoUtils;
import libcore.util.Objects;
import org.xmlpull.v1.XmlPullParser;
@@ -46,6 +55,8 @@
import java.util.ArrayList;
import java.util.List;
+import static android.os.Process.FIRST_APPLICATION_UID;
+
/**
* This class contains the state for one type of settings. It is responsible
* for saving the state asynchronously to an XML file after a mutation and
@@ -63,6 +74,8 @@
private static final String LOG_TAG = "SettingsState";
+ static final String SYSTEM_PACKAGE_NAME = "android";
+
static final int SETTINGS_VERSION_NEW_ENCODING = 121;
private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
@@ -71,27 +84,32 @@
public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000;
- public static final String SYSTEM_PACKAGE_NAME = "android";
-
public static final int VERSION_UNDEFINED = -1;
private static final String TAG_SETTINGS = "settings";
private static final String TAG_SETTING = "setting";
private static final String ATTR_PACKAGE = "package";
+ private static final String ATTR_DEFAULT_SYS_SET = "defaultSysSet";
+ private static final String ATTR_TAG = "tag";
+ private static final String ATTR_TAG_BASE64 = "tagBase64";
private static final String ATTR_VERSION = "version";
private static final String ATTR_ID = "id";
private static final String ATTR_NAME = "name";
- /** Non-binary value will be written in this attribute. */
+ /**
+ * Non-binary value will be written in this attributes.
+ */
private static final String ATTR_VALUE = "value";
+ private static final String ATTR_DEFAULT_VALUE = "defaultValue";
/**
- * KXmlSerializer won't like some characters. We encode such characters in base64 and
- * store in this attribute.
- * NOTE: A null value will have NEITHER ATTR_VALUE nor ATTR_VALUE_BASE64.
+ * KXmlSerializer won't like some characters. We encode such characters
+ * in base64 and store in this attribute.
+ * NOTE: A null value will have *neither* ATTR_VALUE nor ATTR_VALUE_BASE64.
*/
private static final String ATTR_VALUE_BASE64 = "valueBase64";
+ private static final String ATTR_DEFAULT_VALUE_BASE64 = "defaultValueBase64";
// This was used in version 120 and before.
private static final String NULL_VALUE_OLD_STYLE = "null";
@@ -101,12 +119,29 @@
private static final String HISTORICAL_OPERATION_DELETE = "delete";
private static final String HISTORICAL_OPERATION_PERSIST = "persist";
private static final String HISTORICAL_OPERATION_INITIALIZE = "initialize";
+ private static final String HISTORICAL_OPERATION_RESET = "reset";
+
+ private static final String SHELL_PACKAGE_NAME = "shell";
+ private static final String ROOT_PACKAGE_NAME = "root";
+
+ private static final String NULL_VALUE = "null";
+
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static final SparseIntArray sSystemUids = new SparseIntArray();
+
+ @GuardedBy("sLock")
+ private static Signature sSystemSignature;
private final Object mLock;
private final Handler mHandler;
@GuardedBy("mLock")
+ private final Context mContext;
+
+ @GuardedBy("mLock")
private final ArrayMap<String, Setting> mSettings = new ArrayMap<>();
@GuardedBy("mLock")
@@ -118,7 +153,7 @@
@GuardedBy("mLock")
private final File mStatePersistFile;
- private final Setting mNullSetting = new Setting(null, null, null) {
+ private final Setting mNullSetting = new Setting(null, null, false, null, null) {
@Override
public boolean isNull() {
return true;
@@ -149,11 +184,12 @@
@GuardedBy("mLock")
private int mNextHistoricalOpIdx;
- public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage,
- Looper looper) {
+ public SettingsState(Context context, Object lock, File file, int key,
+ int maxBytesPerAppPackage, Looper looper) {
// It is important that we use the same lock as the settings provider
// to ensure multiple mutations on this state are atomicaly persisted
// as the async persistence should be blocked while we make changes.
+ mContext = context;
mLock = lock;
mStatePersistFile = file;
mKey = key;
@@ -241,37 +277,41 @@
}
// The settings provider must hold its lock when calling here.
- public boolean updateSettingLocked(String name, String value, String packageName) {
+ public boolean updateSettingLocked(String name, String value, String tag,
+ boolean makeValue, String packageName) {
if (!hasSettingLocked(name)) {
return false;
}
- return insertSettingLocked(name, value, packageName);
+ return insertSettingLocked(name, value, tag, makeValue, packageName);
}
// The settings provider must hold its lock when calling here.
- public boolean insertSettingLocked(String name, String value, String packageName) {
+ public boolean insertSettingLocked(String name, String value, String tag,
+ boolean makeDefault, String packageName) {
if (TextUtils.isEmpty(name)) {
return false;
}
Setting oldState = mSettings.get(name);
String oldValue = (oldState != null) ? oldState.value : null;
+ String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null;
Setting newState;
if (oldState != null) {
- if (!oldState.update(value, packageName)) {
+ if (!oldState.update(value, makeDefault, packageName, tag)) {
return false;
}
newState = oldState;
} else {
- newState = new Setting(name, value, packageName);
+ newState = new Setting(name, value, makeDefault, packageName, tag);
mSettings.put(name, newState);
}
addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState);
- updateMemoryUsagePerPackageLocked(packageName, oldValue, value);
+ updateMemoryUsagePerPackageLocked(packageName, oldValue, value,
+ oldDefaultValue, newState.getDefaultValue());
scheduleWriteIfNeededLocked();
@@ -292,7 +332,8 @@
Setting oldState = mSettings.remove(name);
- updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null);
+ updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value,
+ null, oldState.defaultValue, null);
addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState);
@@ -302,6 +343,35 @@
}
// The settings provider must hold its lock when calling here.
+ public boolean resetSettingLocked(String name, String packageName) {
+ if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
+ return false;
+ }
+
+ Setting setting = mSettings.get(name);
+
+ Setting oldSetting = new Setting(setting);
+ String oldValue = setting.getValue();
+ String oldDefaultValue = setting.getDefaultValue();
+
+ if (!setting.reset(packageName)) {
+ return false;
+ }
+
+ String newValue = setting.getValue();
+ String newDefaultValue = setting.getDefaultValue();
+
+ updateMemoryUsagePerPackageLocked(setting.packageName, oldValue,
+ newValue, oldDefaultValue, newDefaultValue);
+
+ addHistoricalOperationLocked(HISTORICAL_OPERATION_RESET, oldSetting);
+
+ scheduleWriteIfNeededLocked();
+
+ return true;
+ }
+
+ // The settings provider must hold its lock when calling here.
public void destroyLocked(Runnable callback) {
mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
if (callback != null) {
@@ -364,7 +434,7 @@
}
private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
- String newValue) {
+ String newValue, String oldDefaultValue, String newDefaultValue) {
if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
return;
}
@@ -375,7 +445,10 @@
final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
final int newValueSize = (newValue != null) ? newValue.length() : 0;
- final int deltaSize = newValueSize - oldValueSize;
+ final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
+ final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0;
+ final int deltaSize = newValueSize + newDefaultValueSize
+ - oldValueSize - oldDefaultValueSize;
Integer currentSize = mPackageToMemoryUsage.get(packageName);
final int newSize = Math.max((currentSize != null)
@@ -469,7 +542,8 @@
Setting setting = settings.valueAt(i);
writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(),
- setting.getValue(), setting.getPackageName());
+ setting.getValue(), setting.getDefaultValue(), setting.getPackageName(),
+ setting.getTag(), setting.isDefaultSystemSet());
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
@@ -496,7 +570,8 @@
}
static void writeSingleSetting(int version, XmlSerializer serializer, String id,
- String name, String value, String packageName) throws IOException {
+ String name, String value, String defaultValue, String packageName,
+ String tag, boolean defaultSysSet) throws IOException {
if (id == null || isBinary(id) || name == null || isBinary(name)
|| packageName == null || isBinary(packageName)) {
// This shouldn't happen.
@@ -505,38 +580,46 @@
serializer.startTag(null, TAG_SETTING);
serializer.attribute(null, ATTR_ID, id);
serializer.attribute(null, ATTR_NAME, name);
- setValueAttribute(version, serializer, value);
+ setValueAttribute(ATTR_VALUE, ATTR_VALUE_BASE64,
+ version, serializer, value);
serializer.attribute(null, ATTR_PACKAGE, packageName);
+ if (defaultValue != null) {
+ setValueAttribute(ATTR_DEFAULT_VALUE, ATTR_DEFAULT_VALUE_BASE64,
+ version, serializer, defaultValue);
+ serializer.attribute(null, ATTR_DEFAULT_SYS_SET, Boolean.toString(defaultSysSet));
+ setValueAttribute(ATTR_TAG, ATTR_TAG_BASE64,
+ version, serializer, tag);
+ }
serializer.endTag(null, TAG_SETTING);
}
- static void setValueAttribute(int version, XmlSerializer serializer, String value)
- throws IOException {
+ static void setValueAttribute(String attr, String attrBase64, int version,
+ XmlSerializer serializer, String value) throws IOException {
if (version >= SETTINGS_VERSION_NEW_ENCODING) {
if (value == null) {
// Null value -> No ATTR_VALUE nor ATTR_VALUE_BASE64.
} else if (isBinary(value)) {
- serializer.attribute(null, ATTR_VALUE_BASE64, base64Encode(value));
+ serializer.attribute(null, attrBase64, base64Encode(value));
} else {
- serializer.attribute(null, ATTR_VALUE, value);
+ serializer.attribute(null, attr, value);
}
} else {
// Old encoding.
if (value == null) {
- serializer.attribute(null, ATTR_VALUE, NULL_VALUE_OLD_STYLE);
+ serializer.attribute(null, attr, NULL_VALUE_OLD_STYLE);
} else {
- serializer.attribute(null, ATTR_VALUE, value);
+ serializer.attribute(null, attr, value);
}
}
}
- private String getValueAttribute(XmlPullParser parser) {
+ private String getValueAttribute(XmlPullParser parser, String attr, String base64Attr) {
if (mVersion >= SETTINGS_VERSION_NEW_ENCODING) {
- final String value = parser.getAttributeValue(null, ATTR_VALUE);
+ final String value = parser.getAttributeValue(null, attr);
if (value != null) {
return value;
}
- final String base64 = parser.getAttributeValue(null, ATTR_VALUE_BASE64);
+ final String base64 = parser.getAttributeValue(null, base64Attr);
if (base64 != null) {
return base64Decode(base64);
}
@@ -544,7 +627,7 @@
return null;
} else {
// Old encoding.
- final String stored = parser.getAttributeValue(null, ATTR_VALUE);
+ final String stored = parser.getAttributeValue(null, attr);
if (NULL_VALUE_OLD_STYLE.equals(stored)) {
return null;
} else {
@@ -575,7 +658,7 @@
} catch (XmlPullParserException | IOException e) {
String message = "Failed parsing settings file: " + mStatePersistFile;
Slog.wtf(LOG_TAG, message);
- throw new IllegalStateException(message , e);
+ throw new IllegalStateException(message, e);
} finally {
IoUtils.closeQuietly(in);
}
@@ -615,9 +698,19 @@
if (tagName.equals(TAG_SETTING)) {
String id = parser.getAttributeValue(null, ATTR_ID);
String name = parser.getAttributeValue(null, ATTR_NAME);
- String value = getValueAttribute(parser);
+ String value = getValueAttribute(parser, ATTR_VALUE, ATTR_VALUE_BASE64);
String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
- mSettings.put(name, new Setting(name, value, packageName, id));
+ String defaultValue = getValueAttribute(parser, ATTR_DEFAULT_VALUE,
+ ATTR_DEFAULT_VALUE_BASE64);
+ String tag = null;
+ boolean fromSystem = false;
+ if (defaultValue != null) {
+ fromSystem = Boolean.parseBoolean(parser.getAttributeValue(
+ null, ATTR_DEFAULT_SYS_SET));
+ tag = getValueAttribute(parser, ATTR_TAG, ATTR_TAG_BASE64);
+ }
+ mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag,
+ fromSystem, id));
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
@@ -664,37 +757,54 @@
class Setting {
private String name;
private String value;
+ private String defaultValue;
private String packageName;
private String id;
+ private String tag;
+ // Whether the default is set by the system
+ private boolean defaultSystemSet;
public Setting(Setting other) {
name = other.name;
value = other.value;
+ defaultValue = other.defaultValue;
packageName = other.packageName;
id = other.id;
+ defaultSystemSet = other.defaultSystemSet;
+ tag = other.tag;
}
- public Setting(String name, String value, String packageName) {
- init(name, value, packageName, String.valueOf(mNextId++));
+ public Setting(String name, String value, boolean makeDefault, String packageName,
+ String tag) {
+ this.name = name;
+ update(value, makeDefault, packageName, tag);
}
- public Setting(String name, String value, String packageName, String id) {
+ public Setting(String name, String value, String defaultValue,
+ String packageName, String tag, boolean fromSystem, String id) {
mNextId = Math.max(mNextId, Long.parseLong(id) + 1);
- init(name, value, packageName, id);
+ if (NULL_VALUE.equals(value)) {
+ value = null;
+ }
+ init(name, value, tag, defaultValue, packageName, fromSystem, id);
}
- private void init(String name, String value, String packageName, String id) {
+ private void init(String name, String value, String tag, String defaultValue,
+ String packageName, boolean fromSystem, String id) {
this.name = name;
this.value = value;
+ this.tag = tag;
+ this.defaultValue = defaultValue;
this.packageName = packageName;
this.id = id;
+ this.defaultSystemSet = fromSystem;
}
public String getName() {
return name;
}
- public int getkey() {
+ public int getKey() {
return mKey;
}
@@ -702,10 +812,22 @@
return value;
}
+ public String getTag() {
+ return tag;
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
public String getPackageName() {
return packageName;
}
+ public boolean isDefaultSystemSet() {
+ return defaultSystemSet;
+ }
+
public String getId() {
return id;
}
@@ -714,18 +836,62 @@
return false;
}
- public boolean update(String value, String packageName) {
- if (Objects.equal(value, this.value)) {
+ /** @return whether the value changed */
+ public boolean reset(String packageName) {
+ return update(this.defaultValue, false, packageName, null);
+ }
+
+ public boolean update(String value, boolean setDefault, String packageName, String tag) {
+ if (NULL_VALUE.equals(value)) {
+ value = null;
+ }
+
+ final boolean callerSystem = !isNull() && isSystemPackage(mContext, packageName);
+ // Settings set by the system are always defaults.
+ if (callerSystem) {
+ setDefault = true;
+ }
+
+ String defaultValue = this.defaultValue;
+ boolean defaultSystemSet = this.defaultSystemSet;
+ if (setDefault) {
+ if (!Objects.equal(value, this.defaultValue)
+ && (!defaultSystemSet || callerSystem)) {
+ defaultValue = value;
+ // Default null means no default, so the tag is irrelevant
+ // since it is used to reset a settings subset their defaults.
+ // Also it is irrelevant if the system set the canonical default.
+ if (defaultValue == null) {
+ tag = null;
+ defaultSystemSet = false;
+ }
+ }
+ if (!defaultSystemSet && value != null) {
+ if (callerSystem) {
+ defaultSystemSet = true;
+ }
+ }
+ }
+
+ // Is something gonna change?
+ if (Objects.equal(value, this.value)
+ && Objects.equal(defaultValue, this.defaultValue)
+ && Objects.equal(packageName, this.packageName)
+ && Objects.equal(tag, this.tag)
+ && defaultSystemSet == this.defaultSystemSet) {
return false;
}
- this.value = value;
- this.packageName = packageName;
- this.id = String.valueOf(mNextId++);
+
+ init(name, value, tag, defaultValue, packageName, defaultSystemSet,
+ String.valueOf(mNextId++));
return true;
}
public String toString() {
- return "Setting{name=" + value + " from " + packageName + "}";
+ return "Setting{name=" + name + " value=" + value
+ + (defaultValue != null ? " default=" + defaultValue : "")
+ + " packageName=" + packageName + " tag=" + tag
+ + " defaultSystemSet=" + defaultSystemSet + "}";
}
}
@@ -782,4 +948,74 @@
}
return sb.toString();
}
+
+ public static boolean isSystemPackage(Context context, String packageName) {
+ synchronized (sLock) {
+ if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
+ return true;
+ }
+
+ // Shell and Root are not considered a part of the system
+ if (SHELL_PACKAGE_NAME.equals(packageName)
+ || ROOT_PACKAGE_NAME.equals(packageName)) {
+ return false;
+ }
+
+ final int uid;
+ try {
+ uid = context.getPackageManager().getPackageUid(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+
+ // If the system or a special system UID (like telephony), done.
+ if (UserHandle.getAppId(uid) < FIRST_APPLICATION_UID) {
+ sSystemUids.put(uid, uid);
+ return true;
+ }
+
+ // If already known system component, done.
+ if (sSystemUids.indexOfKey(uid) >= 0) {
+ return true;
+ }
+
+ // If SetupWizard, done.
+ PackageManagerInternal packageManagerInternal = LocalServices.getService(
+ PackageManagerInternal.class);
+ if (packageName.equals(packageManagerInternal.getSetupWizardPackageName())) {
+ sSystemUids.put(uid, uid);
+ return true;
+ }
+
+ // If a persistent system app, done.
+ PackageInfo packageInfo;
+ try {
+ packageInfo = context.getPackageManager().getPackageInfo(
+ packageName, PackageManager.GET_SIGNATURES);
+ if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_PERSISTENT) != 0
+ && (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ sSystemUids.put(uid, uid);
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+
+ // Last check if system signed.
+ if (sSystemSignature == null) {
+ try {
+ sSystemSignature = context.getPackageManager().getPackageInfo(
+ SYSTEM_PACKAGE_NAME, PackageManager.GET_SIGNATURES).signatures[0];
+ } catch (PackageManager.NameNotFoundException e) {
+ /* impossible */
+ return false;
+ }
+ }
+ if (sSystemSignature.equals(packageInfo.signatures[0])) {
+ sSystemUids.put(uid, uid);
+ return true;
+ }
+ return false;
+ }
+ }
}
diff --git a/packages/SettingsProvider/test/Android.mk b/packages/SettingsProvider/test/Android.mk
index ef863e7..918410e 100644
--- a/packages/SettingsProvider/test/Android.mk
+++ b/packages/SettingsProvider/test/Android.mk
@@ -2,11 +2,15 @@
include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+
# Note we statically link SettingsState to do some unit tests. It's not accessible otherwise
# because this test is not an instrumentation test. (because the target runs in the system process.)
LOCAL_SRC_FILES := $(call all-subdir-java-files) \
../src/com/android/providers/settings/SettingsState.java
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
LOCAL_PACKAGE_NAME := SettingsProviderTest
LOCAL_MODULE_TAGS := tests
diff --git a/packages/SettingsProvider/test/AndroidManifest.xml b/packages/SettingsProvider/test/AndroidManifest.xml
index 7a86b5f..71e0b153 100644
--- a/packages/SettingsProvider/test/AndroidManifest.xml
+++ b/packages/SettingsProvider/test/AndroidManifest.xml
@@ -29,7 +29,8 @@
</application>
<instrumentation
- android:name="android.test.InstrumentationTestRunner"
+ android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.providers.setting.test"
android:label="Settings Provider Tests" />
+
</manifest>
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java
index 8e56f47..0454b51 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/BaseSettingsProviderTest.java
@@ -25,14 +25,21 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.test.AndroidTestCase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import libcore.io.Streams;
+import org.junit.runner.RunWith;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.List;
/**
* Base class for the SettingContentProvider tests.
*/
-abstract class BaseSettingsProviderTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+abstract class BaseSettingsProviderTest {
protected static final int SETTING_TYPE_GLOBAL = 1;
protected static final int SETTING_TYPE_SECURE = 2;
protected static final int SETTING_TYPE_SYSTEM = 3;
@@ -48,23 +55,7 @@
Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE
};
- protected int mSecondaryUserId = UserHandle.USER_SYSTEM;
-
- @Override
- public void setContext(Context context) {
- super.setContext(context);
-
- UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
- List<UserInfo> users = userManager.getUsers();
- final int userCount = users.size();
- for (int i = 0; i < userCount; i++) {
- UserInfo user = users.get(i);
- if (!user.isPrimary() && !user.isManagedProfile()) {
- mSecondaryUserId = user.id;
- break;
- }
- }
- }
+ private int mSecondaryUserId = Integer.MIN_VALUE;
protected void setStringViaFrontEndApiSetting(int type, String name, String value, int userId) {
ContentResolver contentResolver = getContext().getContentResolver();
@@ -176,6 +167,163 @@
return null;
}
+ protected static void resetSettingsViaShell(int type, int resetMode) throws IOException {
+ final String modeString;
+ switch (resetMode) {
+ case Settings.RESET_MODE_UNTRUSTED_DEFAULTS: {
+ modeString = "untrusted_defaults";
+ } break;
+
+ case Settings.RESET_MODE_UNTRUSTED_CHANGES: {
+ modeString = "untrusted_clear";
+ } break;
+
+ case Settings.RESET_MODE_TRUSTED_DEFAULTS: {
+ modeString = "trusted_defaults";
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Invalid reset mode: " + resetMode);
+ }
+ }
+
+ switch (type) {
+ case SETTING_TYPE_GLOBAL: {
+ executeShellCommand("settings reset global " + modeString);
+ } break;
+
+ case SETTING_TYPE_SECURE: {
+ executeShellCommand("settings reset secure " + modeString);
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Invalid type: " + type);
+ }
+ }
+ }
+
+ protected static void resetToDefaultsViaShell(int type, String packageName) throws IOException {
+ resetToDefaultsViaShell(type, packageName, null);
+ }
+
+ protected static void resetToDefaultsViaShell(int type, String packageName, String token)
+ throws IOException {
+ switch (type) {
+ case SETTING_TYPE_GLOBAL: {
+ executeShellCommand("settings reset global " + packageName + " " + token);
+ } break;
+
+ case SETTING_TYPE_SECURE: {
+ executeShellCommand("settings reset secure " + packageName + " " + token);
+ } break;
+
+ case SETTING_TYPE_SYSTEM: {
+ executeShellCommand("settings reset system " + packageName + " " + token);
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Invalid type: " + type);
+ }
+ }
+ }
+
+ protected String getSetting(int type, String name) {
+ switch (type) {
+ case SETTING_TYPE_GLOBAL: {
+ return Settings.Global.getString(getContext().getContentResolver(), name);
+ }
+
+ case SETTING_TYPE_SECURE: {
+ return Settings.Secure.getString(getContext().getContentResolver(), name);
+ }
+
+ case SETTING_TYPE_SYSTEM: {
+ return Settings.System.getString(getContext().getContentResolver(), name);
+ }
+
+ default: {
+ throw new IllegalArgumentException("Invalid type: " + type);
+ }
+ }
+ }
+
+ protected void putSetting(int type, String name, String value) {
+ switch (type) {
+ case SETTING_TYPE_GLOBAL: {
+ Settings.Global.putString(getContext().getContentResolver(), name, value);
+ } break;
+
+ case SETTING_TYPE_SECURE: {
+ Settings.Secure.putString(getContext().getContentResolver(), name, value);
+ } break;
+
+ case SETTING_TYPE_SYSTEM: {
+ Settings.System.putString(getContext().getContentResolver(), name, value);
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Invalid type: " + type);
+ }
+ }
+ }
+
+ protected static void setSettingViaShell(int type, String name, String value,
+ boolean makeDefault) throws IOException {
+ setSettingViaShell(type, name, value, null, makeDefault);
+ }
+
+ protected static void setSettingViaShell(int type, String name, String value,
+ String token, boolean makeDefault) throws IOException {
+ switch (type) {
+ case SETTING_TYPE_GLOBAL: {
+ executeShellCommand("settings put global " + name + " "
+ + value + (token != null ? " " + token : "")
+ + (makeDefault ? " default" : ""));
+
+ } break;
+
+ case SETTING_TYPE_SECURE: {
+ executeShellCommand("settings put secure " + name + " "
+ + value + (token != null ? " " + token : "")
+ + (makeDefault ? " default" : ""));
+ } break;
+
+ case SETTING_TYPE_SYSTEM: {
+ executeShellCommand("settings put system " + name + " "
+ + value + (token != null ? " " + token : "")
+ + (makeDefault ? " default" : ""));
+ } break;
+
+ default: {
+ throw new IllegalArgumentException("Invalid type: " + type);
+ }
+ }
+ }
+
+ protected Context getContext() {
+ return InstrumentationRegistry.getContext();
+ }
+
+ protected int getSecondaryUserId() {
+ if (mSecondaryUserId == Integer.MIN_VALUE) {
+ UserManager userManager = (UserManager) getContext()
+ .getSystemService(Context.USER_SERVICE);
+ List<UserInfo> users = userManager.getUsers();
+ final int userCount = users.size();
+ for (int i = 0; i < userCount; i++) {
+ UserInfo user = users.get(i);
+ if (!user.isPrimary() && !user.isManagedProfile()) {
+ mSecondaryUserId = user.id;
+ return mSecondaryUserId;
+ }
+ }
+ }
+ if (mSecondaryUserId == Integer.MIN_VALUE) {
+ mSecondaryUserId = UserHandle.USER_SYSTEM;
+ }
+ return mSecondaryUserId;
+ }
+
protected static Uri getBaseUriForType(int type) {
switch (type) {
case SETTING_TYPE_GLOBAL: {
@@ -195,4 +343,10 @@
}
}
}
+
+ protected static void executeShellCommand(String command) throws IOException {
+ InputStream is = new FileInputStream(InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().executeShellCommand(command).getFileDescriptor());
+ Streams.readFully(is);
+ }
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java
index a09d5fe..d34b9ed 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderPerformanceTest.java
@@ -16,9 +16,13 @@
package com.android.providers.settings;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
+import org.junit.Test;
/**
* Performance tests for the SettingContentProvider.
@@ -32,6 +36,7 @@
private static final long MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS = 20;
+ @Test
public void testSetAndGetPerformanceForGlobalViaFrontEndApi() throws Exception {
// Start with a clean slate.
insertStringViaProviderApi(SETTING_TYPE_GLOBAL,
@@ -76,6 +81,7 @@
< MAX_AVERAGE_SET_AND_GET_SETTING_DURATION_MILLIS);
}
+ @Test
public void testSetAndGetPerformanceForGlobalViaProviderApi() throws Exception {
// Start with a clean slate.
deleteStringViaProviderApi(SETTING_TYPE_GLOBAL, FAKE_SETTING_NAME);
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java
index 8ca1b46..e2a8fba 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderTest.java
@@ -16,6 +16,11 @@
package com.android.providers.settings;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.fail;
+
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.ContentObserver;
@@ -27,6 +32,7 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
+import org.junit.Test;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -46,57 +52,70 @@
private final Object mLock = new Object();
+ @Test
public void testSetAndGetGlobalViaFrontEndApiForSystemUser() throws Exception {
performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
}
+ @Test
public void testSetAndGetGlobalViaFrontEndApiForNonSystemUser() throws Exception {
- if (mSecondaryUserId == UserHandle.USER_SYSTEM) {
+ final int secondaryUserId = getSecondaryUserId();
+ if (secondaryUserId == UserHandle.USER_SYSTEM) {
Log.w(LOG_TAG, "No secondary user. Skipping "
+ "testSetAndGetGlobalViaFrontEndApiForNonSystemUser");
return;
}
- performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, mSecondaryUserId);
+ performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_GLOBAL, secondaryUserId);
}
+ @Test
public void testSetAndGetSecureViaFrontEndApiForSystemUser() throws Exception {
performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, UserHandle.USER_SYSTEM);
}
+ @Test
public void testSetAndGetSecureViaFrontEndApiForNonSystemUser() throws Exception {
- if (mSecondaryUserId == UserHandle.USER_SYSTEM) {
+ final int secondaryUserId = getSecondaryUserId();
+ if (secondaryUserId == UserHandle.USER_SYSTEM) {
Log.w(LOG_TAG, "No secondary user. Skipping "
+ "testSetAndGetSecureViaFrontEndApiForNonSystemUser");
return;
}
- performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, mSecondaryUserId);
+ performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SECURE, secondaryUserId);
}
+ @Test
public void testSetAndGetSystemViaFrontEndApiForSystemUser() throws Exception {
performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, UserHandle.USER_SYSTEM);
}
+ @Test
public void testSetAndGetSystemViaFrontEndApiForNonSystemUser() throws Exception {
- if (mSecondaryUserId == UserHandle.USER_SYSTEM) {
+ final int secondaryUserId = getSecondaryUserId();
+ if (secondaryUserId == UserHandle.USER_SYSTEM) {
Log.w(LOG_TAG, "No secondary user. Skipping "
+ "testSetAndGetSystemViaFrontEndApiForNonSystemUser");
return;
}
- performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, mSecondaryUserId);
+ performSetAndGetSettingTestViaFrontEndApi(SETTING_TYPE_SYSTEM, secondaryUserId);
}
+ @Test
public void testSetAndGetGlobalViaProviderApi() throws Exception {
performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_GLOBAL);
}
+ @Test
public void testSetAndGetSecureViaProviderApi() throws Exception {
performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_SECURE);
}
+ @Test
public void testSetAndGetSystemViaProviderApi() throws Exception {
performSetAndGetSettingTestViaProviderApi(SETTING_TYPE_SYSTEM);
}
+ @Test
public void testSelectAllGlobalViaProviderApi() throws Exception {
setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_GLOBAL,
FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false);
@@ -108,6 +127,7 @@
}
}
+ @Test
public void testSelectAllSecureViaProviderApi() throws Exception {
setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_SECURE,
FAKE_SETTING_NAME, FAKE_SETTING_VALUE, false);
@@ -119,6 +139,7 @@
}
}
+ @Test
public void testSelectAllSystemViaProviderApi() throws Exception {
setSettingViaProviderApiAndAssertSuccessfulChange(SETTING_TYPE_SYSTEM,
FAKE_SETTING_NAME, FAKE_SETTING_VALUE, true);
@@ -130,30 +151,37 @@
}
}
+ @Test
public void testQueryUpdateDeleteGlobalViaProviderApi() throws Exception {
doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_GLOBAL);
}
+ @Test
public void testQueryUpdateDeleteSecureViaProviderApi() throws Exception {
doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_SECURE);
}
+ @Test
public void testQueryUpdateDeleteSystemViaProviderApi() throws Exception {
doTestQueryUpdateDeleteGlobalViaProviderApiForType(SETTING_TYPE_SYSTEM);
}
+ @Test
public void testBulkInsertGlobalViaProviderApi() throws Exception {
toTestBulkInsertViaProviderApiForType(SETTING_TYPE_GLOBAL);
}
+ @Test
public void testBulkInsertSystemViaProviderApi() throws Exception {
toTestBulkInsertViaProviderApiForType(SETTING_TYPE_SYSTEM);
}
+ @Test
public void testBulkInsertSecureViaProviderApi() throws Exception {
toTestBulkInsertViaProviderApiForType(SETTING_TYPE_SECURE);
}
+ @Test
public void testAppCannotRunsSystemOutOfMemoryWritingSystemSettings() throws Exception {
int insertedCount = 0;
try {
@@ -164,6 +192,8 @@
}
fail("Adding app specific settings must be bound.");
} catch (Exception e) {
+ // expected
+ } finally {
for (; insertedCount >= 0; insertedCount--) {
Log.w(LOG_TAG, "Removing app specific setting: " + insertedCount);
deleteStringViaProviderApi(SETTING_TYPE_SYSTEM,
@@ -172,18 +202,22 @@
}
}
+ @Test
public void testQueryStringInBracketsGlobalViaProviderApiForType() throws Exception {
doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_GLOBAL);
}
+ @Test
public void testQueryStringInBracketsSecureViaProviderApiForType() throws Exception {
doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_SECURE);
}
+ @Test
public void testQueryStringInBracketsSystemViaProviderApiForType() throws Exception {
doTestQueryStringInBracketsViaProviderApiForType(SETTING_TYPE_SYSTEM);
}
+ @Test
public void testQueryStringWithAppendedNameToUriViaProviderApi() throws Exception {
// Make sure we have a clean slate.
deleteStringViaProviderApi(SETTING_TYPE_SYSTEM, FAKE_SETTING_NAME);
@@ -206,6 +240,228 @@
}
}
+ @Test
+ public void testResetModePackageDefaultsGlobal() throws Exception {
+ testResetModePackageDefaultsCommon(SETTING_TYPE_GLOBAL);
+ }
+
+ @Test
+ public void testResetModePackageDefaultsSecure() throws Exception {
+ testResetModePackageDefaultsCommon(SETTING_TYPE_SECURE);
+ }
+
+ private void testResetModePackageDefaultsCommon(int type) throws Exception {
+ // Make sure we have a clean slate.
+ setSettingViaShell(type, FAKE_SETTING_NAME, null, true);
+ try {
+ // Set a value but don't make it the default
+ setSettingViaShell(type, FAKE_SETTING_NAME,
+ FAKE_SETTING_VALUE, false);
+
+ // Reset the changes made by the "shell/root" package
+ resetToDefaultsViaShell(type, "shell");
+ resetToDefaultsViaShell(type, "root");
+
+ // Make sure the old APIs don't set defaults
+ assertNull(getSetting(type, FAKE_SETTING_NAME));
+
+ // Set a value and make it the default
+ setSettingViaShell(type, FAKE_SETTING_NAME,
+ FAKE_SETTING_VALUE, true);
+ // Change the setting from the default
+ setSettingViaShell(type, FAKE_SETTING_NAME,
+ FAKE_SETTING_VALUE_2, false);
+
+ // Reset the changes made by this package
+ resetToDefaultsViaShell(type, "shell");
+ resetToDefaultsViaShell(type, "root");
+
+ // Make sure the old APIs don't set defaults
+ assertEquals(FAKE_SETTING_VALUE, getSetting(type, FAKE_SETTING_NAME));
+ } finally {
+ // Make sure we have a clean slate.
+ setSettingViaShell(type, FAKE_SETTING_NAME, null, true);
+ }
+ }
+
+ @Test
+ public void testResetModePackageDefaultsWithTokensGlobal() throws Exception {
+ testResetModePackageDefaultsWithTokensCommon(SETTING_TYPE_GLOBAL);
+ }
+
+ @Test
+ public void testResetModePackageDefaultsWithTokensSecure() throws Exception {
+ testResetModePackageDefaultsWithTokensCommon(SETTING_TYPE_SECURE);
+ }
+
+ private void testResetModePackageDefaultsWithTokensCommon(int type) throws Exception {
+ // Make sure we have a clean slate.
+ setSettingViaShell(type, FAKE_SETTING_NAME, null, true);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ try {
+ // Set a default value
+ setSettingViaShell(type, FAKE_SETTING_NAME,
+ FAKE_SETTING_VALUE, true);
+ // Change the default and associate a token
+ setSettingViaShell(type, FAKE_SETTING_NAME,
+ FAKE_SETTING_VALUE_2, "TOKEN1", false);
+
+ // Set a default value
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE, "TOKEN2", true);
+ // Change the default and associate a token
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE_2, "TOKEN2", false);
+
+ // Reset settings associated with TOKEN1
+ resetToDefaultsViaShell(type, "shell", "TOKEN1");
+ resetToDefaultsViaShell(type, "root", "TOKEN1");
+
+ // Make sure TOKEN1 settings are reset
+ assertEquals(FAKE_SETTING_VALUE, getSetting(type,
+ FAKE_SETTING_NAME));
+
+ // Make sure TOKEN2 settings are not reset
+ assertEquals(FAKE_SETTING_VALUE_2, getSetting(type,
+ FAKE_SETTING_NAME_1));
+
+ // Reset settings associated with TOKEN2
+ resetToDefaultsViaShell(type, "shell", "TOKEN2");
+ resetToDefaultsViaShell(type, "root", "TOKEN2");
+
+ // Make sure TOKEN2 settings are reset
+ assertEquals(FAKE_SETTING_VALUE, getSetting(type,
+ FAKE_SETTING_NAME_1));
+ } finally {
+ // Make sure we have a clean slate.
+ setSettingViaShell(type, FAKE_SETTING_NAME, null, true);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ }
+ }
+
+ @Test
+ public void testResetModeUntrustedDefaultsGlobal() throws Exception {
+ testResetModeUntrustedDefaultsCommon(SETTING_TYPE_GLOBAL);
+ }
+
+ @Test
+ public void testResetModeUntrustedDefaultsSecure() throws Exception {
+ testResetModeUntrustedDefaultsCommon(SETTING_TYPE_SECURE);
+ }
+
+ private void testResetModeUntrustedDefaultsCommon(int type) throws Exception {
+ // Make sure we have a clean slate.
+ putSetting(type, FAKE_SETTING_NAME, null);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ try {
+ // Set a default setting as a trusted component
+ putSetting(type, FAKE_SETTING_NAME, FAKE_SETTING_VALUE);
+ // Change the setting as a trusted component
+ putSetting(type, FAKE_SETTING_NAME, FAKE_SETTING_VALUE_2);
+
+ // Set a default setting as an untrusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE, true);
+ // Change the setting as an untrusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE_2, false);
+
+ // Reset the untrusted changes to defaults
+ resetSettingsViaShell(type,
+ Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+
+ // Check whether only the untrusted changes set to defaults
+ assertEquals(FAKE_SETTING_VALUE_2, getSetting(type, FAKE_SETTING_NAME));
+ assertEquals(FAKE_SETTING_VALUE, getSetting(type, FAKE_SETTING_NAME_1));
+ } finally {
+ // Make sure we have a clean slate.
+ putSetting(type, FAKE_SETTING_NAME, null);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ }
+ }
+
+ @Test
+ public void testResetModeUntrustedClearGlobal() throws Exception {
+ testResetModeUntrustedClearCommon(SETTING_TYPE_GLOBAL);
+ }
+
+ @Test
+ public void testResetModeUntrustedClearSecure() throws Exception {
+ testResetModeUntrustedClearCommon(SETTING_TYPE_SECURE);
+ }
+
+ private void testResetModeUntrustedClearCommon(int type) throws Exception {
+ // Make sure we have a clean slate.
+ putSetting(type, FAKE_SETTING_NAME, null);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ try {
+ // Set a default setting as a trusted component
+ putSetting(type, FAKE_SETTING_NAME, FAKE_SETTING_VALUE);
+ // Change the setting as a trusted component
+ putSetting(type, FAKE_SETTING_NAME, FAKE_SETTING_VALUE_2);
+
+ // Set a default setting as an untrusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE, true);
+ // Change the setting as an untrusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE_2, false);
+
+ // Clear the untrusted changes
+ resetSettingsViaShell(type,
+ Settings.RESET_MODE_UNTRUSTED_CHANGES);
+
+ // Check whether only the untrusted changes set to defaults
+ assertEquals(FAKE_SETTING_VALUE_2, getSetting(type, FAKE_SETTING_NAME));
+ assertNull(getSetting(type, FAKE_SETTING_NAME_1));
+ } finally {
+ // Make sure we have a clean slate.
+ putSetting(type, FAKE_SETTING_NAME, null);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ }
+ }
+
+ @Test
+ public void testResetModeTrustedDefaultsGlobal() throws Exception {
+ testResetModeTrustedDefaultsCommon(SETTING_TYPE_GLOBAL);
+ }
+
+ @Test
+ public void testResetModeTrustedDefaultsSecure() throws Exception {
+ testResetModeTrustedDefaultsCommon(SETTING_TYPE_SECURE);
+ }
+
+ private void testResetModeTrustedDefaultsCommon(int type) throws Exception {
+ // Make sure we have a clean slate.
+ putSetting(type, FAKE_SETTING_NAME, null);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ try {
+ // Set a default setting as a trusted component
+ putSetting(type, FAKE_SETTING_NAME, FAKE_SETTING_VALUE);
+ // Change the setting as a trusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME, FAKE_SETTING_VALUE_2, false);
+
+ // Set a default setting as an untrusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE, true);
+ // Change the setting as an untrusted component
+ setSettingViaShell(type, FAKE_SETTING_NAME_1,
+ FAKE_SETTING_VALUE_2, false);
+
+ // Reset to trusted defaults
+ resetSettingsViaShell(type,
+ Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+ // Check whether snapped to trusted defaults
+ assertEquals(FAKE_SETTING_VALUE, getSetting(type, FAKE_SETTING_NAME));
+ assertNull(getSetting(type, FAKE_SETTING_NAME_1));
+ } finally {
+ // Make sure we have a clean slate.
+ putSetting(type, FAKE_SETTING_NAME, null);
+ setSettingViaShell(type, FAKE_SETTING_NAME_1, null, true);
+ }
+ }
+
private void doTestQueryStringInBracketsViaProviderApiForType(int type) {
// Make sure we have a clean slate.
deleteStringViaProviderApi(type, FAKE_SETTING_NAME);
@@ -341,22 +597,16 @@
private void setSettingViaFrontEndApiAndAssertSuccessfulChange(final int type,
final String name, final String value, final int userId) throws Exception {
- setSettingAndAssertSuccessfulChange(new Runnable() {
- @Override
- public void run() {
- setStringViaFrontEndApiSetting(type, name, value, userId);
- }
+ setSettingAndAssertSuccessfulChange(() -> {
+ setStringViaFrontEndApiSetting(type, name, value, userId);
}, type, name, value, userId);
}
private void setSettingViaProviderApiAndAssertSuccessfulChange(final int type,
final String name, final String value, final boolean withTableRowUri)
throws Exception {
- setSettingAndAssertSuccessfulChange(new Runnable() {
- @Override
- public void run() {
- insertStringViaProviderApi(type, name, value, withTableRowUri);
- }
+ setSettingAndAssertSuccessfulChange(() -> {
+ insertStringViaProviderApi(type, name, value, withTableRowUri);
}, type, name, value, UserHandle.USER_SYSTEM);
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 9964467..3f68554 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -99,10 +99,10 @@
checkWriteSingleSetting(serializer, CRAZY_STRING, null);
SettingsState.writeSingleSetting(
SettingsState.SETTINGS_VERSION_NEW_ENCODING,
- serializer, null, "k", "v", "package");
+ serializer, null, "k", "v", null, "package", null, false);
SettingsState.writeSingleSetting(
SettingsState.SETTINGS_VERSION_NEW_ENCODING,
- serializer, "1", "k", "v", null);
+ serializer, "1", "k", "v", null, null, null, false);
}
private void checkWriteSingleSetting(XmlSerializer serializer, String key, String value)
@@ -115,7 +115,7 @@
// Make sure the XML serializer won't crash.
SettingsState.writeSingleSetting(
SettingsState.SETTINGS_VERSION_NEW_ENCODING,
- serializer, "1", key, value, "package");
+ serializer, "1", key, value, null, "package", null, false);
}
/**
@@ -126,19 +126,19 @@
file.delete();
final Object lock = new Object();
- final SettingsState ssWriter = new SettingsState(lock, file, 1,
+ final SettingsState ssWriter = new SettingsState(getContext(), lock, file, 1,
SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
ssWriter.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING);
- ssWriter.insertSettingLocked("k1", "\u0000", "package");
- ssWriter.insertSettingLocked("k2", "abc", "p2");
- ssWriter.insertSettingLocked("k3", null, "p2");
- ssWriter.insertSettingLocked("k4", CRAZY_STRING, "p3");
+ ssWriter.insertSettingLocked("k1", "\u0000", null, false, "package");
+ ssWriter.insertSettingLocked("k2", "abc", null, false, "p2");
+ ssWriter.insertSettingLocked("k3", null, null, false, "p2");
+ ssWriter.insertSettingLocked("k4", CRAZY_STRING, null, false, "p3");
synchronized (lock) {
ssWriter.persistSyncLocked();
}
- final SettingsState ssReader = new SettingsState(lock, file, 1,
+ final SettingsState ssReader = new SettingsState(getContext(), lock, file, 1,
SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
synchronized (lock) {
assertEquals("\u0000", ssReader.getSettingLocked("k1").getValue());
@@ -165,7 +165,7 @@
"</settings>");
os.close();
- final SettingsState ss = new SettingsState(lock, file, 1,
+ final SettingsState ss = new SettingsState(getContext(), lock, file, 1,
SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
synchronized (lock) {
SettingsState.Setting s;
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 8c80c71..2a895f9 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -20,12 +20,16 @@
<item type="id" name="translation_y_animator_tag"/>
<item type="id" name="translation_z_animator_tag"/>
<item type="id" name="alpha_animator_tag"/>
+ <item type="id" name="scale_x_animator_tag"/>
+ <item type="id" name="scale_y_animator_tag"/>
<item type="id" name="top_inset_animator_tag"/>
<item type="id" name="height_animator_tag"/>
<item type="id" name="shadow_alpha_animator_tag"/>
<item type="id" name="translation_x_animator_end_value_tag"/>
<item type="id" name="translation_y_animator_end_value_tag"/>
<item type="id" name="translation_z_animator_end_value_tag"/>
+ <item type="id" name="scale_x_animator_end_value_tag"/>
+ <item type="id" name="scale_y_animator_end_value_tag"/>
<item type="id" name="alpha_animator_end_value_tag"/>
<item type="id" name="top_inset_animator_end_value_tag"/>
<item type="id" name="height_animator_end_value_tag"/>
@@ -33,6 +37,8 @@
<item type="id" name="translation_x_animator_start_value_tag"/>
<item type="id" name="translation_y_animator_start_value_tag"/>
<item type="id" name="translation_z_animator_start_value_tag"/>
+ <item type="id" name="scale_x_animator_start_value_tag"/>
+ <item type="id" name="scale_y_animator_start_value_tag"/>
<item type="id" name="alpha_animator_start_value_tag"/>
<item type="id" name="top_inset_animator_start_value_tag"/>
<item type="id" name="height_animator_start_value_tag"/>
diff --git a/packages/SystemUI/src/com/android/systemui/Interpolators.java b/packages/SystemUI/src/com/android/systemui/Interpolators.java
index fd3bd92..9b5577c 100644
--- a/packages/SystemUI/src/com/android/systemui/Interpolators.java
+++ b/packages/SystemUI/src/com/android/systemui/Interpolators.java
@@ -23,6 +23,8 @@
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
+import com.android.systemui.statusbar.stack.HeadsUpAppearInterpolator;
+
/**
* Utility class to receive interpolators from
*/
@@ -37,6 +39,8 @@
public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f);
+ public static final Interpolator HEADS_UP_APPEAR = new HeadsUpAppearInterpolator();
+ public static final Interpolator ICON_OVERSHOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
/**
* Interpolator to be used when animating a move based on a click. Pair with enough duration.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index cf8d332..d1ab96d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -29,6 +29,7 @@
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityManager;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
@@ -37,6 +38,7 @@
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.policy.AccessibilityController;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -99,6 +101,7 @@
private final int mTintedRippleColor;
private final int mLowPriorityRippleColor;
protected final int mNormalRippleColor;
+ private final AccessibilityManager mAccessibilityManager;
private boolean mDimmed;
private boolean mDark;
@@ -199,6 +202,7 @@
mNormalRippleColor = context.getColor(
R.color.notification_ripple_untinted_color);
mFalsingManager = FalsingManager.getInstance(context);
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
}
@Override
@@ -223,13 +227,17 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (mDimmed && !mActivated
- && ev.getActionMasked() == MotionEvent.ACTION_DOWN && disallowSingleClick(ev)) {
+ if (mDimmed && !mActivated && ev.getActionMasked() == MotionEvent.ACTION_DOWN
+ && disallowSingleClick(ev) && !isTouchExplorationEnabled()) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
+ private boolean isTouchExplorationEnabled() {
+ return mAccessibilityManager.isTouchExplorationEnabled();
+ }
+
protected boolean disallowSingleClick(MotionEvent ev) {
return false;
}
@@ -241,7 +249,7 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result;
- if (mDimmed) {
+ if (mDimmed && !isTouchExplorationEnabled()) {
boolean wasActivated = mActivated;
result = handleTouchEventDimmed(event);
if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 5601425..8562fa1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -101,6 +101,7 @@
import com.android.systemui.recents.Recents;
import com.android.systemui.statusbar.NotificationData.Entry;
import com.android.systemui.statusbar.NotificationGuts.OnGutsClosedListener;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NavigationBarView;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -116,12 +117,12 @@
import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.Stack;
public abstract class BaseStatusBar extends SystemUI implements
CommandQueue.Callbacks, ActivatableNotificationView.OnActivatedListener,
ExpandableNotificationRow.ExpansionLogger, NotificationData.Environment,
- ExpandableNotificationRow.OnExpandClickListener,
- OnGutsClosedListener {
+ ExpandableNotificationRow.OnExpandClickListener, OnGutsClosedListener {
public static final String TAG = "StatusBar";
public static final boolean DEBUG = false;
public static final boolean MULTIUSER_DEBUG = false;
@@ -175,6 +176,9 @@
// for heads up notifications
protected HeadsUpManager mHeadsUpManager;
+ // handling reordering
+ protected VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
+
protected int mCurrentUserId = 0;
final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>();
@@ -975,18 +979,11 @@
}
}
- protected void bindDismissListener(final ExpandableNotificationRow row) {
- row.setOnDismissListener(new View.OnClickListener() {
- public void onClick(View v) {
- // Accessibility feedback
- v.announceForAccessibility(
- mContext.getString(R.string.accessibility_notification_dismissed));
- performRemoveNotification(row.getStatusBarNotification(), false /* removeView */);
- }
- });
+ protected void bindDismissRunnable(final ExpandableNotificationRow row) {
+ row.setOnDismissRunnable(() -> performRemoveNotification(row.getStatusBarNotification()));
}
- protected void performRemoveNotification(StatusBarNotification n, boolean removeView) {
+ protected void performRemoveNotification(StatusBarNotification n) {
final String pkg = n.getPackageName();
final String tag = n.getTag();
final int id = n.getId();
@@ -996,14 +993,8 @@
if (FORCE_REMOTE_INPUT_HISTORY
&& mKeysKeptForRemoteInput.contains(n.getKey())) {
mKeysKeptForRemoteInput.remove(n.getKey());
- removeView = true;
}
- if (mRemoteInputEntriesToRemoveOnCollapse.remove(mNotificationData.get(n.getKey()))) {
- removeView = true;
- }
- if (removeView) {
- removeNotification(n.getKey(), null);
- }
+ removeNotification(n.getKey(), null);
} catch (RemoteException ex) {
// system process is dead if we're here.
@@ -1656,7 +1647,7 @@
}
workAroundBadLayerDrawableOpacity(row);
- bindDismissListener(row);
+ bindDismissRunnable(row);
// NB: the large icon is now handled entirely by the template
@@ -1978,8 +1969,7 @@
Runnable removeRunnable = new Runnable() {
@Override
public void run() {
- performRemoveNotification(parentToCancelFinal,
- true);
+ performRemoveNotification(parentToCancelFinal);
}
};
if (isCollapsing()) {
@@ -2192,8 +2182,7 @@
* Updates expanded, dimmed and locked states of notification rows.
*/
protected void updateRowStates() {
- ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
- final int N = activeNotifications.size();
+ final int N = mStackScroller.getChildCount();
int visibleNotifications = 0;
boolean onKeyguard = mState == StatusBarState.KEYGUARD;
@@ -2202,21 +2191,27 @@
maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
}
mStackScroller.setMaxDisplayedNotifications(maxNotifications);
- for (int i = 0; i < N; i++) {
- NotificationData.Entry entry = activeNotifications.get(i);
+ Stack<ExpandableNotificationRow> stack = new Stack<>();
+ for (int i = N - 1; i >= 0; i--) {
+ View child = mStackScroller.getChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ stack.push((ExpandableNotificationRow) child);
+ }
+ while(!stack.isEmpty()) {
+ ExpandableNotificationRow row = stack.pop();
+ NotificationData.Entry entry = row.getEntry();
boolean childNotification = mGroupManager.isChildInGroupWithSummary(entry.notification);
if (onKeyguard) {
- entry.row.setOnKeyguard(true);
+ row.setOnKeyguard(true);
} else {
- entry.row.setOnKeyguard(false);
- entry.row.setSystemExpanded(visibleNotifications == 0 && !childNotification);
+ row.setOnKeyguard(false);
+ row.setSystemExpanded(visibleNotifications == 0 && !childNotification);
}
int userId = entry.notification.getUserId();
boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
entry.notification) && !entry.row.isRemoved();
- boolean childWithVisibleSummary = childNotification
- && mGroupManager.getGroupSummary(entry.notification).getVisibility()
- == View.VISIBLE;
boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
if (suppressedSummary
|| (isLockscreenPublicMode(userId) && !mShowLockscreenNotifications)
@@ -2234,6 +2229,14 @@
visibleNotifications++;
}
}
+ if (row.isSummaryWithChildren()) {
+ List<ExpandableNotificationRow> notificationChildren =
+ row.getNotificationChildren();
+ int size = notificationChildren.size();
+ for (int i = size - 1; i >= 0; i--) {
+ stack.push(notificationChildren.get(i));
+ }
+ }
}
mStackScroller.changeViewPosition(mDismissView, mStackScroller.getChildCount() - 1);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index e89fe55..8ae84cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -51,6 +51,7 @@
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.statusbar.notification.HybridNotificationView;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.stack.AnimationProperties;
@@ -112,7 +113,6 @@
private NotificationContentView mPrivateLayout;
private int mMaxExpandHeight;
private int mHeadsUpHeight;
- private View mVetoButton;
private int mNotificationColor;
private ExpansionLogger mLogger;
private String mLoggingKey;
@@ -197,6 +197,7 @@
private boolean mIconsVisible = true;
private boolean mAboveShelf;
private boolean mIsLastChild;
+ private Runnable mOnDismissRunnable;
public boolean isGroupExpansionChanging() {
if (isChildInGroup()) {
@@ -469,10 +470,15 @@
* Apply the order given in the list to the children.
*
* @param childOrder the new list order
+ * @param visualStabilityManager
+ * @param callback the callback to invoked in case it is not allowed
* @return whether the list order has changed
*/
- public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
- return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder);
+ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+ VisualStabilityManager visualStabilityManager,
+ VisualStabilityManager.Callback callback) {
+ return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder,
+ visualStabilityManager, callback);
}
public void getChildrenStates(StackScrollState resultState) {
@@ -807,11 +813,13 @@
}
public void performDismiss() {
- mVetoButton.performClick();
+ if (mOnDismissRunnable != null) {
+ mOnDismissRunnable.run();
+ }
}
- public void setOnDismissListener(OnClickListener listener) {
- mVetoButton.setOnClickListener(listener);
+ public void setOnDismissRunnable(Runnable onDismissRunnable) {
+ mOnDismissRunnable = onDismissRunnable;
}
public View getNotificationIcon() {
@@ -921,6 +929,10 @@
return topPadding;
}
+ public float getContentTranslation() {
+ return mPrivateLayout.getTranslationY();
+ }
+
public interface ExpansionLogger {
public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
}
@@ -1021,10 +1033,6 @@
mTranslateableViews.add(mChildrenContainer);
}
});
- mVetoButton = findViewById(R.id.veto);
- mVetoButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- mVetoButton.setContentDescription(mContext.getString(
- R.string.accessibility_remove_notification));
// Add the views that we translate to reveal the gear
mTranslateableViews = new ArrayList<View>();
@@ -1032,16 +1040,11 @@
mTranslateableViews.add(getChildAt(i));
}
// Remove views that don't translate
- mTranslateableViews.remove(mVetoButton);
mTranslateableViews.remove(mSettingsIconRowStub);
mTranslateableViews.remove(mChildrenContainerStub);
mTranslateableViews.remove(mGutsStub);
}
- public View getVetoButton() {
- return mVetoButton;
- }
-
public void resetTranslation() {
if (mTranslateAnim != null) {
mTranslateAnim.cancel();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 0a138b8..296c788 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -45,6 +45,8 @@
public static final boolean SHOW_AMBIENT_ICONS = true;
private static final boolean USE_ANIMATIONS_WHEN_OPENING =
SystemProperties.getBoolean("debug.icon_opening_animations", true);
+ private static final boolean ICON_ANMATIONS_WHILE_SCROLLING
+ = SystemProperties.getBoolean("debug.icon_scroll_animations", true);
private ViewInvertHelper mViewInvertHelper;
private boolean mDark;
private NotificationIconContainer mShelfIcons;
@@ -63,6 +65,7 @@
private NotificationIconContainer mCollapsedIcons;
private int mScrollFastThreshold;
private int mStatusBarState;
+ private float mMaxShelfEnd;
public NotificationShelf(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -168,6 +171,7 @@
}
mShelfState.hasItemsInStableShelf = lastViewState.inShelf;
mShelfState.hidden = !mAmbientState.isShadeExpanded();
+ mShelfState.maxShelfEnd = maxShelfEnd;
} else {
mShelfState.hidden = true;
mShelfState.location = ExpandableViewState.LOCATION_GONE;
@@ -202,9 +206,11 @@
int colorTwoBefore = NO_COLOR;
int previousColor = NO_COLOR;
float transitionAmount = 0.0f;
- boolean scrollingFast = mAmbientState.getCurrentScrollVelocity() > mScrollFastThreshold
+ float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
+ boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
|| (mAmbientState.isExpansionChanging()
&& Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
+ boolean scrolling = currentScrollVelocity > 0;
boolean expandingAnimated = mAmbientState.isExpansionChanging()
&& !mAmbientState.isPanelTracking();
int baseZHeight = mAmbientState.getBaseZHeight();
@@ -233,7 +239,7 @@
}
}
updateNotificationClipHeight(row, notificationClipEnd);
- float inShelfAmount = updateIconAppearance(row, expandAmount, scrollingFast,
+ float inShelfAmount = updateIconAppearance(row, expandAmount, scrolling, scrollingFast,
expandingAnimated, isLastChild);
numViewsInShelf += inShelfAmount;
int ownColorUntinted = row.getBackgroundColorWithoutTint();
@@ -284,11 +290,13 @@
* @return the icon amount how much this notification is in the shelf;
*/
private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount,
- boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
+ boolean scrolling, boolean scrollingFast, boolean expandingAnimated,
+ boolean isLastChild) {
// Let calculate how much the view is in the shelf
float viewStart = row.getTranslationY();
int fullHeight = row.getActualHeight() + mPaddingBetweenElements;
float iconTransformDistance = getIntrinsicHeight() * 1.5f;
+ iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount);
if (isLastChild) {
fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight());
iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight()
@@ -321,26 +329,25 @@
fullTransitionAmount = 0.0f;
iconTransitionAmount = 0.0f;
}
- updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount, scrollingFast,
- expandingAnimated, isLastChild);
+ updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount,
+ iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild);
return fullTransitionAmount;
}
private void updateIconPositioning(ExpandableNotificationRow row, float iconTransitionAmount,
- float fullTransitionAmount, boolean scrollingFast, boolean expandingAnimated,
- boolean isLastChild) {
+ float fullTransitionAmount, float iconTransformDistance, boolean scrolling,
+ boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
StatusBarIconView icon = row.getEntry().expandedIcon;
NotificationIconContainer.IconState iconState = getIconState(icon);
if (iconState == null) {
return;
}
float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f;
- if (clampedAmount == iconTransitionAmount) {
- iconState.keepClampedPosition = false;
- }
if (clampedAmount == fullTransitionAmount) {
- iconState.useFullTransitionAmount = fullTransitionAmount == 0.0f || scrollingFast
- || expandingAnimated;
+ iconState.useFullTransitionAmount = scrollingFast || expandingAnimated
+ || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling);
+ iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING
+ && fullTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging();
iconState.translateContent = mMaxLayoutHeight - getTranslationY()
- getIntrinsicHeight() > 0;
}
@@ -350,49 +357,41 @@
iconState.useFullTransitionAmount = true;
}
float transitionAmount;
- boolean needCannedAnimation = iconState.clampedAppearAmount == 1.0f
- && clampedAmount == 0.0f;
- if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount) {
+ if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
+ || iconState.useLinearTransitionAmount) {
transitionAmount = iconTransitionAmount;
- } else if (iconState.keepClampedPosition
- && iconState.clampedAppearAmount != clampedAmount) {
- // We animated to the clamped amount but then decided to go the other way. Let's
- // animate it to the new position
- transitionAmount = iconTransitionAmount;
- iconState.needsCannedAnimation = true;
- iconState.keepClampedPosition = false;
- } else if (needCannedAnimation || iconState.keepClampedPosition
- || iconState.iconAppearAmount == 1.0f) {
- // We need to perform a canned animation since we crossed the treshhold
- transitionAmount = clampedAmount;
- iconState.keepClampedPosition = iconState.keepClampedPosition || needCannedAnimation;
- iconState.needsCannedAnimation = needCannedAnimation;
} else {
- transitionAmount = iconTransitionAmount;
+ // We take the clamped position instead
+ transitionAmount = clampedAmount;
+ iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount;
}
iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING
|| iconState.useFullTransitionAmount
? fullTransitionAmount
: transitionAmount;
iconState.clampedAppearAmount = clampedAmount;
- setIconTransformationAmount(row, transitionAmount);
float contentTransformationAmount = isLastChild || iconState.translateContent
? iconTransitionAmount
: 0.0f;
row.setContentTransformationAmount(contentTransformationAmount, isLastChild);
- }
-
- private boolean isLastChild(ExpandableNotificationRow row) {
- return row == mAmbientState.getLastVisibleBackgroundChild();
+ setIconTransformationAmount(row, transitionAmount, iconTransformDistance,
+ clampedAmount != transitionAmount);
}
private void setIconTransformationAmount(ExpandableNotificationRow row,
- float transitionAmount) {
+ float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation) {
StatusBarIconView icon = row.getEntry().expandedIcon;
NotificationIconContainer.IconState iconState = getIconState(icon);
View rowIcon = row.getNotificationIcon();
- float notificationIconPosition = row.getTranslationY();
+ float notificationIconPosition = row.getTranslationY() + row.getContentTranslation();
+ if (usingLinearInterpolation) {
+ // If we interpolate from the notification position, this might lead to a slightly
+ // odd interpolation, since the notification position changes as well. Let's interpolate
+ // from a fixed distance. We can only do this if we don't animate and the icon is
+ // always in the interpolated positon.
+ notificationIconPosition = mMaxShelfEnd - getIntrinsicHeight() - iconTransformDistance;
+ }
float notificationIconSize = 0.0f;
int iconTopPadding;
if (rowIcon != null) {
@@ -404,15 +403,8 @@
notificationIconPosition += iconTopPadding;
float shelfIconPosition = getTranslationY() + icon.getTop();
shelfIconPosition += ((1.0f - icon.getIconScale()) * icon.getHeight()) / 2.0f;
- float transitionDistance = getIntrinsicHeight() * 1.5f;
- if (row == mAmbientState.getLastVisibleBackgroundChild()) {
- transitionDistance = Math.min(transitionDistance, row.getMinHeight()
- - getIntrinsicHeight());
- }
- float transformationStartPosition = getTranslationY() - transitionDistance;
float iconYTranslation = NotificationUtils.interpolate(
- Math.min(notificationIconPosition, transformationStartPosition + iconTopPadding)
- - shelfIconPosition,
+ notificationIconPosition - shelfIconPosition,
0,
transitionAmount);
float shelfIconSize = icon.getHeight() * icon.getIconScale();
@@ -557,21 +549,28 @@
: View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
+ public void setMaxShelfEnd(float maxShelfEnd) {
+ mMaxShelfEnd = maxShelfEnd;
+ }
+
private class ShelfState extends ExpandableViewState {
private float openedAmount;
private boolean hasItemsInStableShelf;
+ private float maxShelfEnd;
@Override
public void applyToView(View view) {
super.applyToView(view);
- updateAppearance();
+ setMaxShelfEnd(maxShelfEnd);
setOpenedAmount(openedAmount);
+ updateAppearance();
setHasItemsInStableShelf(hasItemsInStableShelf);
}
@Override
public void animateTo(View child, AnimationProperties properties) {
super.animateTo(child, properties);
+ setMaxShelfEnd(maxShelfEnd);
setOpenedAmount(openedAmount);
updateAppearance();
setHasItemsInStableShelf(hasItemsInStableShelf);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
new file mode 100644
index 0000000..80ba943
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ViewState;
+
+/**
+ * An animator to animate properties
+ */
+public class PropertyAnimator {
+
+ public static <T extends View> void startAnimation(final T view,
+ AnimatableProperty animatableProperty, float newEndValue,
+ AnimationProperties properties) {
+ Property<T, Float> property = animatableProperty.getProperty();
+ int animationStartTag = animatableProperty.getAnimationStartTag();
+ int animationEndTag = animatableProperty.getAnimationEndTag();
+ Float previousStartValue = ViewState.getChildTag(view, animationStartTag);
+ Float previousEndValue = ViewState.getChildTag(view, animationEndTag);
+ if (previousEndValue != null && previousEndValue == newEndValue) {
+ return;
+ }
+ int animatorTag = animatableProperty.getAnimatorTag();
+ ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag);
+ AnimationFilter filter = properties.getAnimationFilter();
+ if (!filter.shouldAnimateProperty(property)) {
+ // just a local update was performed
+ if (previousAnimator != null) {
+ // we need to increase all animation keyframes of the previous animator by the
+ // relative change to the end value
+ PropertyValuesHolder[] values = previousAnimator.getValues();
+ float relativeDiff = newEndValue - previousEndValue;
+ float newStartValue = previousStartValue + relativeDiff;
+ values[0].setFloatValues(newStartValue, newEndValue);
+ view.setTag(animationStartTag, newStartValue);
+ view.setTag(animationEndTag, newEndValue);
+ previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+ return;
+ } else {
+ // no new animation needed, let's just apply the value
+ property.set(view, newEndValue);
+ return;
+ }
+ }
+
+ Float currentValue = property.get(view);
+ ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue);
+ animator.addUpdateListener(
+ animation -> property.set(view, (Float) animation.getAnimatedValue()));
+ Interpolator customInterpolator = properties.getCustomInterpolator(view, property);
+ Interpolator interpolator = customInterpolator != null ? customInterpolator
+ : Interpolators.FAST_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long newDuration = ViewState.cancelAnimatorAndGetNewDuration(properties.duration,
+ previousAnimator);
+ animator.setDuration(newDuration);
+ if (properties.delay > 0 && (previousAnimator == null
+ || previousAnimator.getAnimatedFraction() == 0)) {
+ animator.setStartDelay(properties.delay);
+ }
+ AnimatorListenerAdapter listener = properties.getAnimationFinishListener();
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ // remove the tag when the animation is finished
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setTag(animatorTag, null);
+ view.setTag(animationStartTag, null);
+ view.setTag(animationEndTag, null);
+ }
+ });
+ ViewState.startAnimator(animator, listener);
+ view.setTag(animatorTag, animator);
+ view.setTag(animationStartTag, currentValue);
+ view.setTag(animationEndTag, newEndValue);
+ }
+
+ public interface AnimatableProperty {
+ int getAnimationStartTag();
+ int getAnimationEndTag();
+ int getAnimatorTag();
+ Property getProperty();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
new file mode 100644
index 0000000..4a52acc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An object that can determine the visibility of a Notification.
+ */
+public interface VisibilityLocationProvider {
+
+ /**
+ * @return whether the view is in a visible location right now.
+ */
+ boolean isInVisibleLocation(ExpandableNotificationRow row);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisualStabilityManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisualStabilityManager.java
new file mode 100644
index 0000000..5047041
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisualStabilityManager.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification;
+
+import android.support.v4.util.ArraySet;
+import android.view.View;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+
+import java.util.ArrayList;
+
+/**
+ * A manager that ensures that notifications are visually stable. It will suppress reorderings
+ * and reorder at the right time when they are out of view.
+ */
+public class VisualStabilityManager implements OnHeadsUpChangedListener {
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+
+ private boolean mPanelExpanded;
+ private boolean mScreenOn;
+ private boolean mReorderingAllowed;
+ private VisibilityLocationProvider mVisibilityLocationProvider;
+ private ArraySet<View> mAllowedReorderViews = new ArraySet<>();
+ private ArraySet<View> mAddedChildren = new ArraySet<>();
+
+ /**
+ * Add a callback to invoke when reordering is allowed again.
+ * @param callback
+ */
+ public void addReorderingAllowedCallback(Callback callback) {
+ if (mCallbacks.contains(callback)) {
+ return;
+ }
+ mCallbacks.add(callback);
+ }
+
+ /**
+ * Set the panel to be expanded.
+ */
+ public void setPanelExpanded(boolean expanded) {
+ mPanelExpanded = expanded;
+ updateReorderingAllowed();
+ }
+
+ /**
+ * @param screenOn whether the screen is on
+ */
+ public void setScreenOn(boolean screenOn) {
+ mScreenOn = screenOn;
+ updateReorderingAllowed();
+ }
+
+ private void updateReorderingAllowed() {
+ boolean reorderingAllowed = !mScreenOn || !mPanelExpanded;
+ boolean changed = reorderingAllowed && !mReorderingAllowed;
+ mReorderingAllowed = reorderingAllowed;
+ if (changed) {
+ notifyCallbacks();
+ }
+ }
+
+ private void notifyCallbacks() {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ Callback callback = mCallbacks.get(i);
+ callback.onReorderingAllowed();
+ }
+ mCallbacks.clear();
+ }
+
+ /**
+ * @return whether reordering is currently allowed in general.
+ */
+ public boolean isReorderingAllowed() {
+ return mReorderingAllowed;
+ }
+
+ /**
+ * @return whether a specific notification is allowed to reorder. Certain notifications are
+ * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added
+ * notifications or heads-up notifications that are out of view.
+ */
+ public boolean canReorderNotification(ExpandableNotificationRow row) {
+ if (mReorderingAllowed) {
+ return true;
+ }
+ if (mAddedChildren.contains(row)) {
+ return true;
+ }
+ if (mAllowedReorderViews.contains(row)
+ && !mVisibilityLocationProvider.isInVisibleLocation(row)) {
+ return true;
+ }
+ return false;
+ }
+
+ public void setVisibilityLocationProvider(
+ VisibilityLocationProvider visibilityLocationProvider) {
+ mVisibilityLocationProvider = visibilityLocationProvider;
+ }
+
+ public void onReorderingFinished() {
+ mAllowedReorderViews.clear();
+ mAddedChildren.clear();
+ }
+
+ @Override
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ if (isHeadsUp) {
+ // Heads up notifications should in general be allowed to reorder if they are out of
+ // view and stay at the current location if they aren't.
+ mAllowedReorderViews.add(entry.row);
+ }
+ }
+
+ /**
+ * Notify the visual stability manager that a new view was added and should be allowed to
+ * reorder next time.
+ */
+ public void notifyViewAddition(View view) {
+ mAddedChildren.add(view);
+ }
+
+ public interface Callback {
+ /**
+ * Called when reordering is allowed again.
+ */
+ void onReorderingAllowed();
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
index 2c8339a..dd84dea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java
@@ -23,6 +23,7 @@
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -35,7 +36,7 @@
/**
* A class to handle notifications and their corresponding groups.
*/
-public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChangedListener {
+public class NotificationGroupManager implements OnHeadsUpChangedListener {
private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
private OnGroupChangeListener mListener;
@@ -415,6 +416,10 @@
child = getIsolatedChild(sbn.getGroupKey());
}
if (child != null) {
+ if (child.row.keepInParent() || child.row.isRemoved() || child.row.isDismissed()) {
+ // the notification is actually already removed, no need to do heads-up on it.
+ return;
+ }
if (mHeadsUpManager.isHeadsUp(child.key)) {
mHeadsUpManager.updateNotification(child, true);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index d323e4f..9fb5980 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -22,13 +22,17 @@
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
+import android.util.Property;
import android.view.View;
+import android.view.animation.Interpolator;
+import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.stack.AnimationFilter;
import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.HeadsUpAppearInterpolator;
import com.android.systemui.statusbar.stack.ViewState;
import java.util.HashMap;
@@ -58,24 +62,25 @@
}.setDuration(200);
private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
- private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha();
- // TODO: add scale
+ private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha()
+ .animateScale();
@Override
public AnimationFilter getAnimationFilter() {
return mAnimationFilter;
}
- }.setDuration(CANNED_ANIMATION_DURATION);
+
+ }.setDuration(CANNED_ANIMATION_DURATION)
+ .setCustomInterpolator(View.TRANSLATION_Y, Interpolators.ICON_OVERSHOT);
private static final AnimationProperties mTempProperties = new AnimationProperties() {
private AnimationFilter mAnimationFilter = new AnimationFilter();
- // TODO: add scale
@Override
public AnimationFilter getAnimationFilter() {
return mAnimationFilter;
}
- }.setDuration(CANNED_ANIMATION_DURATION);
+ };
private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
@@ -409,8 +414,8 @@
public int visibleState;
public boolean justAdded = true;
public boolean needsCannedAnimation;
- public boolean keepClampedPosition;
public boolean useFullTransitionAmount;
+ public boolean useLinearTransitionAmount;
public boolean translateContent;
@Override
@@ -440,8 +445,11 @@
AnimationFilter animationFilter = mTempProperties.getAnimationFilter();
animationFilter.reset();
animationFilter.combineFilter(ICON_ANIMATION_PROPERTIES.getAnimationFilter());
+ mTempProperties.resetCustomInterpolators();
+ mTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
if (animationProperties != null) {
animationFilter.combineFilter(animationProperties.getAnimationFilter());
+ mTempProperties.combineCustomInterpolators(animationProperties);
}
animationProperties = mTempProperties;
animationProperties.setDuration(CANNED_ANIMATION_DURATION);
@@ -455,6 +463,7 @@
AnimationFilter animationFilter = mTempProperties.getAnimationFilter();
animationFilter.reset();
animationFilter.animateX();
+ mTempProperties.resetCustomInterpolators();
animationProperties = mTempProperties;
animationProperties.setDuration(CANNED_ANIMATION_DURATION);
animate = true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index a239cb6..7f6322a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -63,6 +63,7 @@
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -72,7 +73,7 @@
ExpandableView.OnHeightChangedListener,
View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
- HeadsUpManager.OnHeadsUpChangedListener, QS.HeightListener {
+ OnHeadsUpChangedListener, QS.HeightListener {
private static final boolean DEBUG = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index 55dd578..7bb075b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -857,7 +857,7 @@
public void collapse(boolean delayed, float speedUpFactor) {
if (DEBUG) logf("collapse: " + this);
- if (!isFullyCollapsed() && !mTracking && !mClosing) {
+ if (canPanelBeCollapsed()) {
cancelHeightAnimator();
notifyExpandingStarted();
@@ -872,6 +872,10 @@
}
}
+ public boolean canPanelBeCollapsed() {
+ return !isFullyCollapsed() && !mTracking && !mClosing;
+ }
+
private final Runnable mFlingCollapseRunnable = new Runnable() {
@Override
public void run() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index eb4c631..70eb23c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -153,7 +153,6 @@
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.activity.AppTransitionFinishedEvent;
import com.android.systemui.recents.events.activity.UndockingTaskEvent;
-import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.stackdivider.WindowManagerProxy;
import com.android.systemui.statusbar.ActivatableNotificationView;
@@ -174,6 +173,7 @@
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.SignalClusterView;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
import com.android.systemui.statusbar.policy.AccessibilityController;
import com.android.systemui.statusbar.policy.BatteryController;
@@ -191,8 +191,8 @@
import com.android.systemui.statusbar.policy.LocationControllerImpl;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.NetworkControllerImpl;
-import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.NextAlarmControllerImpl;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.policy.PreviewInflater;
import com.android.systemui.statusbar.policy.RotationLockControllerImpl;
import com.android.systemui.statusbar.policy.SecurityControllerImpl;
@@ -203,7 +203,6 @@
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
.OnChildLocationsChangedListener;
import com.android.systemui.statusbar.stack.StackStateAnimator;
-import com.android.systemui.statusbar.stack.ExpandableViewState;
import com.android.systemui.volume.VolumeComponent;
import java.io.FileDescriptor;
@@ -218,7 +217,7 @@
public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,
- HeadsUpManager.OnHeadsUpChangedListener {
+ OnHeadsUpChangedListener, VisualStabilityManager.Callback {
static final String TAG = "PhoneStatusBar";
public static final boolean DEBUG = BaseStatusBar.DEBUG;
public static final boolean SPEW = false;
@@ -575,9 +574,6 @@
*/
protected boolean mStartedGoingToSleep;
- private static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN
- | ExpandableViewState.LOCATION_MAIN_AREA;
-
private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
new OnChildLocationsChangedListener() {
@Override
@@ -626,8 +622,7 @@
for (int i = 0; i < N; i++) {
Entry entry = activeNotifications.get(i);
String key = entry.notification.getKey();
- boolean isVisible =
- (mStackScroller.getChildLocation(entry.row) & VISIBLE_LOCATIONS) != 0;
+ boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
if (isVisible) {
@@ -784,9 +779,11 @@
mHeadsUpManager.addListener(this);
mHeadsUpManager.addListener(mNotificationPanel);
mHeadsUpManager.addListener(mGroupManager);
+ mHeadsUpManager.addListener(mVisualStabilityManager);
mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
mNotificationData.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setHeadsUpManager(mHeadsUpManager);
+ mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager);
if (MULTIUSER_DEBUG) {
mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(
@@ -816,6 +813,7 @@
mStackScroller.setGroupManager(mGroupManager);
mStackScroller.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setOnGroupChangeListener(mStackScroller);
+ mVisualStabilityManager.setVisibilityLocationProvider(mStackScroller);
inflateShelf();
inflateEmptyShadeView();
@@ -1749,12 +1747,12 @@
}
@Override
- protected void performRemoveNotification(StatusBarNotification n, boolean removeView) {
+ protected void performRemoveNotification(StatusBarNotification n) {
Entry entry = mNotificationData.get(n.getKey());
if (mRemoteInputController.isRemoteInputActive(entry)) {
mRemoteInputController.removeRemoteInput(entry, null);
}
- super.performRemoveNotification(n, removeView);
+ super.performRemoveNotification(n);
}
@Override
@@ -1783,6 +1781,11 @@
final int N = activeNotifications.size();
for (int i=0; i<N; i++) {
Entry ent = activeNotifications.get(i);
+ if (ent.row.isDismissed() || ent.row.isRemoved()) {
+ // we don't want to update removed notifications because they could
+ // temporarily become children if they were isolated before.
+ continue;
+ }
int vis = ent.notification.getNotification().visibility;
int userId = ent.notification.getUserId();
@@ -1849,6 +1852,7 @@
for (int i=0; i<toShow.size(); i++) {
View v = toShow.get(i);
if (v.getParent() == null) {
+ mVisualStabilityManager.notifyViewAddition(v);
mStackScroller.addView(v);
}
}
@@ -1870,12 +1874,17 @@
if (child != targetChild) {
// Oops, wrong notification at this position. Put the right one
// here and advance both lists.
- mStackScroller.changeViewPosition(targetChild, i);
+ if (mVisualStabilityManager.canReorderNotification(targetChild)) {
+ mStackScroller.changeViewPosition(targetChild, i);
+ } else {
+ mVisualStabilityManager.addReorderingAllowedCallback(this);
+ }
}
j++;
}
+ mVisualStabilityManager.onReorderingFinished();
// clear the map again for the next usage
mTmpChildOrderMap.clear();
@@ -1922,13 +1931,21 @@
childIndex++) {
ExpandableNotificationRow childView = orderedChildren.get(childIndex);
if (children == null || !children.contains(childView)) {
+ if (childView.getParent() != null) {
+ Log.wtf(TAG, "trying to add a notification child that already has " +
+ "a parent. class:" + childView.getParent().getClass() +
+ "\n child: " + childView);
+ // This shouldn't happen. We can recover by removing it though.
+ ((ViewGroup) childView.getParent()).removeView(childView);
+ }
+ mVisualStabilityManager.notifyViewAddition(childView);
parent.addChildNotification(childView, childIndex);
mStackScroller.notifyGroupChildAdded(childView);
}
}
// Finally after removing and adding has been beformed we can apply the order.
- orderChanged |= parent.applyChildOrder(orderedChildren);
+ orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager, this);
}
if (orderChanged) {
mStackScroller.generateChildOrderChangedEvent();
@@ -2005,7 +2022,7 @@
}
private void updateSpeedBumpIndex() {
- int speedBumpIndex = -1;
+ int speedBumpIndex = 0;
int currentIndex = 0;
final int N = mStackScroller.getChildCount();
for (int i = 0; i < N; i++) {
@@ -2014,17 +2031,12 @@
continue;
}
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
- if (mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
- speedBumpIndex = currentIndex;
- break;
- }
currentIndex++;
+ if (!mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
+ speedBumpIndex = currentIndex;
+ }
}
- boolean noAmbient = false;
- if (speedBumpIndex == -1) {
- speedBumpIndex = currentIndex;
- noAmbient = true;
- }
+ boolean noAmbient = speedBumpIndex == N;
mStackScroller.updateSpeedBumpIndex(speedBumpIndex, noAmbient);
}
@@ -2692,7 +2704,7 @@
public void setPanelExpanded(boolean isExpanded) {
mStatusBarWindowManager.setPanelExpanded(isExpanded);
-
+ mVisualStabilityManager.setPanelExpanded(isExpanded);
if (isExpanded && getBarState() != StatusBarState.KEYGUARD) {
if (DEBUG) {
Log.v(TAG, "clearing notification effects from setPanelExpanded");
@@ -2730,6 +2742,11 @@
return mDozeScrimController.isPulsing();
}
+ @Override
+ public void onReorderingAllowed() {
+ updateNotifications();
+ }
+
/**
* All changes to the status bar and notifications funnel through here and are batched.
*/
@@ -2883,7 +2900,7 @@
}
}
- if (mStatusBarWindow != null) {
+ if (mStatusBarWindow != null && mNotificationPanel.canPanelBeCollapsed()) {
// release focus immediately to kick off focus change transition
mStatusBarWindowManager.setStatusBarFocusable(false);
@@ -2954,7 +2971,6 @@
mNotificationPanel.closeQs();
mExpandedVisible = false;
-
visibilityChanged(false);
// Shrink the window to the size of the status bar only
@@ -4622,6 +4638,11 @@
public void onClosingFinished() {
runPostCollapseRunnables();
+ if (!isPanelFullyCollapsed()) {
+ // if we set it not to be focusable when collapsing, we have to undo it when we aborted
+ // the closing
+ mStatusBarWindowManager.setStatusBarFocusable(true);
+ }
}
public void onUnlockHintStarted() {
@@ -4916,6 +4937,7 @@
mWakeUpComingFromTouch = false;
mWakeUpTouchLocation = null;
mStackScroller.setAnimationsEnabled(false);
+ mVisualStabilityManager.setScreenOn(false);
updateVisibleToUser();
if (mLaunchCameraOnFinishedGoingToSleep) {
mLaunchCameraOnFinishedGoingToSleep = false;
@@ -4934,6 +4956,7 @@
public void onStartedWakingUp() {
mDeviceInteractive = true;
mStackScroller.setAnimationsEnabled(true);
+ mVisualStabilityManager.setScreenOn(true);
mNotificationPanel.setTouchDisabled(false);
updateVisibleToUser();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 944495e..517551d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -35,7 +35,7 @@
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.ScrimView;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.ViewState;
/**
@@ -43,7 +43,7 @@
* security method gets shown).
*/
public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
- HeadsUpManager.OnHeadsUpChangedListener {
+ OnHeadsUpChangedListener {
public static final long ANIMATION_DURATION = 220;
public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR
= new PathInterpolator(0f, 0, 0.7f, 1f);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index c21c493..3fa6065 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -22,6 +22,7 @@
import android.os.Handler;
import android.os.SystemClock;
import android.provider.Settings;
+import android.support.v4.util.ArraySet;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pools;
@@ -33,6 +34,7 @@
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
@@ -48,7 +50,8 @@
* A manager which handles heads up notifications which is a special mode where
* they simply peek from the top of the screen.
*/
-public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener {
+public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener,
+ VisualStabilityManager.Callback {
private static final String TAG = "HeadsUpManager";
private static final boolean DEBUG = false;
private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
@@ -96,6 +99,8 @@
private boolean mReleaseOnExpandFinish;
private boolean mTrackingHeadsUp;
private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
+ private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
+ = new ArraySet<>();
private boolean mIsExpanded;
private boolean mHasPinnedNotification;
private int[] mTmpTwoArray = new int[2];
@@ -104,6 +109,7 @@
private boolean mIsObserving;
private boolean mRemoteInputActive;
private float mExpandedHeight;
+ private VisualStabilityManager mVisualStabilityManager;
public HeadsUpManager(final Context context, View statusBarWindowView,
NotificationGroupManager groupManager) {
@@ -603,6 +609,21 @@
}
}
+ @Override
+ public void onReorderingAllowed() {
+ for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
+ if (isHeadsUp(entry.key)) {
+ // Maybe the heads-up was removed already
+ removeHeadsUpEntry(entry);
+ }
+ }
+ mEntriesToRemoveWhenReorderingAllowed.clear();
+ }
+
+ public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
+ mVisualStabilityManager = visualStabilityManager;
+ }
+
/**
* This represents a notification and how long it is in a heads up mode. It also manages its
* lifecycle automatically when created.
@@ -623,7 +644,10 @@
mRemoveHeadsUpRunnable = new Runnable() {
@Override
public void run() {
- if (!mTrackingHeadsUp) {
+ if (!mVisualStabilityManager.isReorderingAllowed()) {
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
+ mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this);
+ } else if (!mTrackingHeadsUp) {
removeHeadsUpEntry(entry);
} else {
mEntriesToRemoveAfterExpand.add(entry);
@@ -647,6 +671,9 @@
if (mEntriesToRemoveAfterExpand.contains(entry)) {
mEntriesToRemoveAfterExpand.remove(entry);
}
+ if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
+ mEntriesToRemoveWhenReorderingAllowed.remove(entry);
+ }
if (!isSticky()) {
long finishTime = postTime + mHeadsUpNotificationDecay;
long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
@@ -716,30 +743,4 @@
}
}
- public interface OnHeadsUpChangedListener {
- /**
- * The state whether there exist pinned heads-ups or not changed.
- *
- * @param inPinnedMode whether there are any pinned heads-ups
- */
- void onHeadsUpPinnedModeChanged(boolean inPinnedMode);
-
- /**
- * A notification was just pinned to the top.
- */
- void onHeadsUpPinned(ExpandableNotificationRow headsUp);
-
- /**
- * A notification was just unpinned from the top.
- */
- void onHeadsUpUnPinned(ExpandableNotificationRow headsUp);
-
- /**
- * A notification just became a heads up or turned back to its normal state.
- *
- * @param entry the entry of the changed notification
- * @param isHeadsUp whether the notification is now a headsUp notification
- */
- void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
new file mode 100644
index 0000000..5444f06
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * A listener to heads up changes
+ */
+public interface OnHeadsUpChangedListener {
+ /**
+ * The state whether there exist pinned heads-ups or not changed.
+ *
+ * @param inPinnedMode whether there are any pinned heads-ups
+ */
+ default void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {}
+
+ /**
+ * A notification was just pinned to the top.
+ */
+ default void onHeadsUpPinned(ExpandableNotificationRow headsUp) {}
+
+ /**
+ * A notification was just unpinned from the top.
+ */
+ default void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {}
+
+ /**
+ * A notification just became a heads up or turned back to its normal state.
+ *
+ * @param entry the entry of the changed notification
+ * @param isHeadsUp whether the notification is now a headsUp notification
+ */
+ default void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
index 38bb40e..34fa658 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
@@ -16,7 +16,12 @@
package com.android.systemui.statusbar.stack;
+import android.support.v4.util.ArraySet;
+import android.util.Property;
+import android.view.View;
+
import java.util.ArrayList;
+import java.util.HashSet;
/**
* Filters the animations for only a certain type of properties.
@@ -37,12 +42,19 @@
boolean hasDarkEvent;
boolean hasHeadsUpDisappearClickEvent;
int darkAnimationOriginIndex;
+ private ArraySet<Property> mAnimatedProperties = new ArraySet<>();
public AnimationFilter animateAlpha() {
animateAlpha = true;
return this;
}
+ public AnimationFilter animateScale() {
+ animate(View.SCALE_X);
+ animate(View.SCALE_Y);
+ return this;
+ }
+
public AnimationFilter animateX() {
animateX = true;
return this;
@@ -132,6 +144,7 @@
animateHideSensitive |= filter.animateHideSensitive;
animateShadowAlpha |= filter.animateShadowAlpha;
hasDelays |= filter.hasDelays;
+ mAnimatedProperties.addAll(filter.mAnimatedProperties);
}
public void reset() {
@@ -151,5 +164,16 @@
hasHeadsUpDisappearClickEvent = false;
darkAnimationOriginIndex =
NotificationStackScrollLayout.AnimationEvent.DARK_ANIMATION_ORIGIN_INDEX_ABOVE;
+ mAnimatedProperties.clear();
+ }
+
+ public AnimationFilter animate(Property property) {
+ mAnimatedProperties.add(property);
+ return this;
+ }
+
+ public boolean shouldAnimateProperty(Property property) {
+ // TODO: migrate all existing animators to properties
+ return mAnimatedProperties.contains(property);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationProperties.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationProperties.java
index 0de774d..ebb0a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationProperties.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationProperties.java
@@ -17,16 +17,20 @@
package com.android.systemui.statusbar.stack;
import android.animation.AnimatorListenerAdapter;
+import android.util.ArrayMap;
import android.util.Property;
import android.view.View;
import android.view.animation.Interpolator;
+import java.util.HashMap;
+
/**
* Properties for a View animation
*/
public class AnimationProperties {
public long duration;
public long delay;
+ private ArrayMap<Property, Interpolator> mInterpolatorMap;
/**
* @return an animation filter for this animation.
@@ -50,7 +54,29 @@
* Get a custom interpolator for a property instead of the normal one.
*/
public Interpolator getCustomInterpolator(View child, Property property) {
- return null;
+ return mInterpolatorMap != null ? mInterpolatorMap.get(property) : null;
+ }
+
+
+ public void combineCustomInterpolators(AnimationProperties iconAnimationProperties) {
+ ArrayMap<Property, Interpolator> map = iconAnimationProperties.mInterpolatorMap;
+ if (map != null) {
+ if (mInterpolatorMap == null) {
+ mInterpolatorMap = new ArrayMap<>();
+ }
+ mInterpolatorMap.putAll(map);
+ }
+ }
+
+ /**
+ * Set a custom interpolator to use for all views for a property.
+ */
+ public AnimationProperties setCustomInterpolator(Property property, Interpolator interpolator) {
+ if (mInterpolatorMap == null) {
+ mInterpolatorMap = new ArrayMap<>();
+ }
+ mInterpolatorMap.put(property, interpolator);
+ return this;
}
public AnimationProperties setDuration(long duration) {
@@ -62,4 +88,9 @@
this.delay = delay;
return this;
}
+
+ public AnimationProperties resetCustomInterpolators() {
+ mInterpolatorMap = null;
+ return this;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ExpandableViewState.java
index 7854c98..e0fd481 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ExpandableViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ExpandableViewState.java
@@ -80,6 +80,12 @@
*/
public static final int LOCATION_GONE = 0x40;
+ /**
+ * The visible locations of a view.
+ */
+ public static final int VISIBLE_LOCATIONS = ExpandableViewState.LOCATION_FIRST_HUN
+ | ExpandableViewState.LOCATION_MAIN_AREA;
+
public int height;
public boolean dimmed;
public boolean dark;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
index 22709f6..b4cbec1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -38,6 +38,7 @@
import com.android.systemui.statusbar.notification.HybridNotificationView;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.NotificationViewWrapper;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import java.util.ArrayList;
@@ -322,9 +323,13 @@
* Apply the order given in the list to the children.
*
* @param childOrder the new list order
+ * @param visualStabilityManager
+ * @param callback
* @return whether the list order has changed
*/
- public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
+ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
+ VisualStabilityManager visualStabilityManager,
+ VisualStabilityManager.Callback callback) {
if (childOrder == null) {
return false;
}
@@ -333,9 +338,13 @@
ExpandableNotificationRow child = mChildren.get(i);
ExpandableNotificationRow desiredChild = childOrder.get(i);
if (child != desiredChild) {
- mChildren.remove(desiredChild);
- mChildren.add(i, desiredChild);
- result = true;
+ if (visualStabilityManager.canReorderNotification(desiredChild)) {
+ mChildren.remove(desiredChild);
+ mChildren.add(i, desiredChild);
+ result = true;
+ } else {
+ visualStabilityManager.addReorderingAllowedCallback(callback);
+ }
}
}
updateExpansionStates();
@@ -478,7 +487,9 @@
childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
}
childState.location = parentState.location;
+ childState.inShelf = parentState.inShelf;
yPosition += intrinsicHeight;
+
}
if (mOverflowNumber != null) {
ExpandableNotificationRow overflowView = mChildren.get(Math.min(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 1dce8b6..ec44f19 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -76,6 +76,7 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.phone.ScrimController;
@@ -93,7 +94,7 @@
public class NotificationStackScrollLayout extends ViewGroup
implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener,
- SettingsIconRowListener, ScrollContainer {
+ SettingsIconRowListener, ScrollContainer, VisibilityLocationProvider {
public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
private static final String TAG = "StackScroller";
@@ -537,21 +538,19 @@
mListener = listener;
}
- /**
- * Returns the location the given child is currently rendered at.
- *
- * @param child the child to get the location for
- * @return one of {@link ExpandableViewState}'s <code>LOCATION_*</code> constants
- */
- public int getChildLocation(View child) {
- ExpandableViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
+ @Override
+ public boolean isInVisibleLocation(ExpandableNotificationRow row) {
+ ExpandableViewState childViewState = mCurrentStackScrollState.getViewStateForView(row);
if (childViewState == null) {
- return ExpandableViewState.LOCATION_UNKNOWN;
+ return false;
}
- if (childViewState.gone) {
- return ExpandableViewState.LOCATION_GONE;
+ if ((childViewState.location &= ExpandableViewState.VISIBLE_LOCATIONS) == 0) {
+ return false;
}
- return childViewState.location;
+ if (row.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ return true;
}
private void setMaxLayoutHeight(int maxLayoutHeight) {
@@ -4668,5 +4667,4 @@
return length;
}
}
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
index 1f29b4f..55085e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -53,7 +53,6 @@
public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
public static final int ANIMATION_DELAY_HEADS_UP = 120;
- private final Interpolator mHeadsUpAppearInterpolator;
private final int mGoToFullShadeAppearingTranslation;
private final ExpandableViewState mTmpState = new ExpandableViewState();
private final AnimationProperties mAnimationProperties;
@@ -83,7 +82,6 @@
mGoToFullShadeAppearingTranslation =
hostLayout.getContext().getResources().getDimensionPixelSize(
R.dimen.go_to_full_shade_appearing_translation);
- mHeadsUpAppearInterpolator = new HeadsUpAppearInterpolator();
mAnimationProperties = new AnimationProperties() {
@Override
public AnimationFilter getAnimationFilter() {
@@ -103,7 +101,7 @@
@Override
public Interpolator getCustomInterpolator(View child, Property property) {
if (mHeadsUpAppearChildren.contains(child) && View.TRANSLATION_Y.equals(property)) {
- return mHeadsUpAppearInterpolator;
+ return Interpolators.HEADS_UP_APPEAR;
}
return null;
}
@@ -253,7 +251,13 @@
View viewAfterChangingView = noNextView
? mHostLayout.getLastChildNotGone()
: event.viewAfterChangingView;
-
+ if (viewAfterChangingView == null) {
+ // This can happen when the last view in the list is removed.
+ // Since the shelf is still around and the only view, the code still goes
+ // in here and tries to calculate the delay for it when case its properties
+ // have changed.
+ continue;
+ }
int nextIndex = finalState
.getViewStateForView(viewAfterChangingView).notGoneIndex;
if (ownIndex >= nextIndex) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
index b747592..e0a6159 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java
@@ -21,12 +21,14 @@
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
+import android.util.Property;
import android.view.View;
import android.view.animation.Interpolator;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.policy.HeadsUpManager;
/**
@@ -60,6 +62,54 @@
private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag;
private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag;
+ private static final PropertyAnimator.AnimatableProperty SCALE_X_PROPERTY
+ = new PropertyAnimator.AnimatableProperty() {
+
+ @Override
+ public int getAnimationStartTag() {
+ return R.id.scale_x_animator_start_value_tag;
+ }
+
+ @Override
+ public int getAnimationEndTag() {
+ return R.id.scale_x_animator_end_value_tag;
+ }
+
+ @Override
+ public int getAnimatorTag() {
+ return R.id.scale_x_animator_tag;
+ }
+
+ @Override
+ public Property getProperty() {
+ return View.SCALE_X;
+ }
+ };
+
+ private static final PropertyAnimator.AnimatableProperty SCALE_Y_PROPERTY
+ = new PropertyAnimator.AnimatableProperty() {
+
+ @Override
+ public int getAnimationStartTag() {
+ return R.id.scale_y_animator_start_value_tag;
+ }
+
+ @Override
+ public int getAnimationEndTag() {
+ return R.id.scale_y_animator_end_value_tag;
+ }
+
+ @Override
+ public int getAnimatorTag() {
+ return R.id.scale_y_animator_tag;
+ }
+
+ @Override
+ public Property getProperty() {
+ return View.SCALE_Y;
+ }
+ };
+
public float alpha;
public float xTranslation;
public float yTranslation;
@@ -125,13 +175,19 @@
}
// apply scaleX
- if (view.getScaleX() != this.scaleX) {
- view.setScaleX(this.scaleX);
+ boolean animatingScaleX = isAnimating(view, SCALE_X_PROPERTY);
+ if (animatingScaleX) {
+ updateAnimation(view, SCALE_X_PROPERTY, scaleX);
+ } else if (view.getScaleX() != scaleX) {
+ view.setScaleX(scaleX);
}
// apply scaleY
- if (view.getScaleY() != this.scaleY) {
- view.setScaleY(this.scaleY);
+ boolean animatingScaleY = isAnimating(view, SCALE_Y_PROPERTY);
+ if (animatingScaleY) {
+ updateAnimation(view, SCALE_Y_PROPERTY, scaleY);
+ } else if (view.getScaleY() != scaleY) {
+ view.setScaleY(scaleY);
}
boolean becomesInvisible = this.alpha == 0.0f || (this.hidden && !isAnimating(view));
@@ -179,13 +235,23 @@
if (isAnimating(view, TAG_ANIMATOR_ALPHA)) {
return true;
}
+ if (isAnimating(view, SCALE_X_PROPERTY)) {
+ return true;
+ }
+ if (isAnimating(view, SCALE_Y_PROPERTY)) {
+ return true;
+ }
return false;
}
- private boolean isAnimating(View view, int tag) {
+ private static boolean isAnimating(View view, int tag) {
return getChildTag(view, tag) != null;
}
+ public static boolean isAnimating(View view, PropertyAnimator.AnimatableProperty property) {
+ return getChildTag(view, property.getAnimatorTag()) != null;
+ }
+
/**
* Start an animation to this viewstate
* @param child the view to animate
@@ -226,6 +292,20 @@
abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z);
}
+ // start scaleX animation
+ if (child.getScaleX() != scaleX) {
+ PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, scaleX, animationProperties);
+ } else {
+ abortAnimation(child, SCALE_X_PROPERTY.getAnimatorTag());
+ }
+
+ // start scaleX animation
+ if (child.getScaleY() != scaleY) {
+ PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, scaleY, animationProperties);
+ } else {
+ abortAnimation(child, SCALE_Y_PROPERTY.getAnimatorTag());
+ }
+
// start alpha animation
if (alphaChanging) {
startAlphaAnimation(child, animationProperties);
@@ -320,6 +400,11 @@
startZTranslationAnimation(view, NO_NEW_ANIMATIONS);
}
+ private void updateAnimation(View view, PropertyAnimator.AnimatableProperty property,
+ float endValue) {
+ PropertyAnimator.startAnimation(view, property, endValue, NO_NEW_ANIMATIONS);
+ }
+
private void startZTranslationAnimation(final View child, AnimationProperties properties) {
Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
@@ -514,7 +599,7 @@
}
}
- protected void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
+ public static void startAnimator(Animator animator, AnimatorListenerAdapter listener) {
if (listener != null) {
// Even if there's a delay we'd want to notify it of the start immediately.
listener.onAnimationStart(animator);
@@ -540,7 +625,8 @@
* @param previousAnimator the animator which was running before
* @return the new duration
*/
- protected long cancelAnimatorAndGetNewDuration(long duration, ValueAnimator previousAnimator) {
+ public static long cancelAnimatorAndGetNewDuration(long duration,
+ ValueAnimator previousAnimator) {
long newDuration = duration;
if (previousAnimator != null) {
// We take either the desired length of the new animation or the remaining time of
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java
new file mode 100644
index 0000000..193250f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notification/PropertyAnimatorTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.notification;
+
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+import android.util.FloatProperty;
+import android.util.Property;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
+import com.android.systemui.statusbar.stack.ViewState;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PropertyAnimatorTest extends SysuiTestCase {
+
+ private View mView;
+ private FloatProperty<View> mEffectiveProperty = new FloatProperty<View>("TEST") {
+ public float mValue = 100;
+
+ @Override
+ public void setValue(View view, float value) {
+ mValue = value;
+ }
+
+ @Override
+ public Float get(View object) {
+ return mValue;
+ }
+ };
+ private PropertyAnimator.AnimatableProperty mProperty
+ = new PropertyAnimator.AnimatableProperty() {
+
+ @Override
+ public int getAnimationStartTag() {
+ return R.id.scale_x_animator_start_value_tag;
+ }
+
+ @Override
+ public int getAnimationEndTag() {
+ return R.id.scale_x_animator_end_value_tag;
+ }
+
+ @Override
+ public int getAnimatorTag() {
+ return R.id.scale_x_animator_tag;
+ }
+
+ @Override
+ public Property getProperty() {
+ return mEffectiveProperty;
+ }
+ };
+ private AnimatorListenerAdapter mFinishListener = mock(AnimatorListenerAdapter.class);
+ private AnimationProperties mAnimationProperties = new AnimationProperties() {
+ @Override
+ public AnimationFilter getAnimationFilter() {
+ return mAnimationFilter;
+ }
+
+ @Override
+ public AnimatorListenerAdapter getAnimationFinishListener() {
+ return mFinishListener;
+ }
+ }.setDuration(200);
+ private AnimationFilter mAnimationFilter = new AnimationFilter();
+ private Interpolator mTestInterpolator = Interpolators.ALPHA_IN;
+
+
+ @Before
+ @UiThreadTest
+ public void setUp() {
+ mView = new View(getContext());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAnimationStarted() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 200, mAnimationProperties);
+ assertTrue(ViewState.isAnimating(mView, mProperty));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testNoAnimationStarted() {
+ mAnimationFilter.reset();
+ PropertyAnimator.startAnimation(mView, mProperty, 200, mAnimationProperties);
+ assertFalse(ViewState.isAnimating(mView, mProperty));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testEndValueUpdated() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationEndTag()),
+ Float.valueOf(200f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testStartTagUpdated() {
+ mEffectiveProperty.set(mView, 100f);
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationStartTag()),
+ Float.valueOf(100f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testValueIsSetUnAnimated() {
+ mAnimationFilter.reset();
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ assertEquals(Float.valueOf(200f), mEffectiveProperty.get(mView));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAnimationToRightValueUpdated() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ mAnimationFilter.reset();
+ PropertyAnimator.startAnimation(mView, mProperty, 220f, mAnimationProperties);
+ assertTrue(ViewState.isAnimating(mView, mProperty));
+ assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationEndTag()),
+ Float.valueOf(220f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAnimationToRightValueUpdateAnimated() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 220f, mAnimationProperties);
+ assertTrue(ViewState.isAnimating(mView, mProperty));
+ assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationEndTag()),
+ Float.valueOf(220f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testStartTagShiftedWhenChanging() {
+ mEffectiveProperty.set(mView, 100f);
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ mAnimationFilter.reset();
+ PropertyAnimator.startAnimation(mView, mProperty, 220f, mAnimationProperties);
+ assertEquals(ViewState.getChildTag(mView, mProperty.getAnimationStartTag()),
+ Float.valueOf(120f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUsingDuration() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ mAnimationProperties.setDuration(500);
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+ assertNotNull(animator);
+ assertEquals(animator.getDuration(), 500);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUsingDelay() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ mAnimationProperties.setDelay(200);
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+ assertNotNull(animator);
+ assertEquals(animator.getStartDelay(), 200);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUsingInterpolator() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ mAnimationProperties.setCustomInterpolator(mEffectiveProperty, mTestInterpolator);
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+ assertNotNull(animator);
+ assertEquals(animator.getInterpolator(), mTestInterpolator);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUsingListener() {
+ mAnimationFilter.reset();
+ mAnimationFilter.animate(mProperty.getProperty());
+ mAnimationProperties.setCustomInterpolator(mEffectiveProperty, mTestInterpolator);
+ PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties);
+ ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag());
+ assertNotNull(animator);
+ assertTrue(animator.getListeners().contains(mFinishListener));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java
new file mode 100644
index 0000000..be6290b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notification/VisualStabilityManagerTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.notification;
+
+import android.service.notification.StatusBarNotification;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VisualStabilityManagerTest {
+
+ private VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
+ private VisualStabilityManager.Callback mCallback = mock(VisualStabilityManager.Callback.class);
+ private VisibilityLocationProvider mLocationProvider = mock(VisibilityLocationProvider.class);
+ private ExpandableNotificationRow mRow = mock(ExpandableNotificationRow.class);
+ private NotificationData.Entry mEntry;
+
+ @Before
+ public void setUp() {
+ mVisualStabilityManager.setVisibilityLocationProvider(mLocationProvider);
+ mEntry = new NotificationData.Entry(mock(StatusBarNotification.class));
+ mEntry.row = mRow;
+ }
+
+ @Test
+ public void testPanelExpansion() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ mVisualStabilityManager.setPanelExpanded(false);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ @Test
+ public void testScreenOn() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ mVisualStabilityManager.setScreenOn(false);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ @Test
+ public void testReorderingAllowedChangesScreenOn() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), false);
+ mVisualStabilityManager.setScreenOn(false);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), true);
+ }
+
+ @Test
+ public void testReorderingAllowedChangesPanel() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), false);
+ mVisualStabilityManager.setPanelExpanded(false);
+ assertEquals(mVisualStabilityManager.isReorderingAllowed(), true);
+ }
+
+ @Test
+ public void testCallBackCalledScreenOn() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.addReorderingAllowedCallback(mCallback);
+ mVisualStabilityManager.setScreenOn(false);
+ verify(mCallback).onReorderingAllowed();
+ }
+
+ @Test
+ public void testCallBackCalledPanelExpanded() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.addReorderingAllowedCallback(mCallback);
+ mVisualStabilityManager.setPanelExpanded(false);
+ verify(mCallback).onReorderingAllowed();
+ }
+
+ @Test
+ public void testCallBackExactlyOnce() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.addReorderingAllowedCallback(mCallback);
+ mVisualStabilityManager.setScreenOn(false);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.setScreenOn(false);
+ verify(mCallback).onReorderingAllowed();
+ }
+
+ @Test
+ public void testAddedCanReorder() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ mVisualStabilityManager.notifyViewAddition(mRow);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ @Test
+ public void testReorderingVisibleHeadsUpNotAllowed() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ when(mLocationProvider.isInVisibleLocation(anyObject())).thenReturn(true);
+ mVisualStabilityManager.onHeadsUpStateChanged(mEntry, true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ }
+
+ @Test
+ public void testReorderingVisibleHeadsUpAllowed() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ when(mLocationProvider.isInVisibleLocation(anyObject())).thenReturn(false);
+ mVisualStabilityManager.onHeadsUpStateChanged(mEntry, true);
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), true);
+ }
+
+ @Test
+ public void testReorderingVisibleHeadsUpAllowedOnce() {
+ mVisualStabilityManager.setPanelExpanded(true);
+ mVisualStabilityManager.setScreenOn(true);
+ when(mLocationProvider.isInVisibleLocation(anyObject())).thenReturn(false);
+ mVisualStabilityManager.onHeadsUpStateChanged(mEntry, true);
+ mVisualStabilityManager.onReorderingFinished();
+ assertEquals(mVisualStabilityManager.canReorderNotification(mRow), false);
+ }
+}
diff --git a/services/core/java/com/android/server/NetworkScoreService.java b/services/core/java/com/android/server/NetworkScoreService.java
index f712f12..c64aa8e 100644
--- a/services/core/java/com/android/server/NetworkScoreService.java
+++ b/services/core/java/com/android/server/NetworkScoreService.java
@@ -40,7 +40,7 @@
import android.net.RecommendationResult;
import android.net.ScoredNetwork;
import android.net.Uri;
-import android.net.wifi.WifiConfiguration;
+import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
@@ -319,47 +319,54 @@
" is not the active scorer.");
}
- // Separate networks by type.
- Map<Integer, List<ScoredNetwork>> networksByType = new ArrayMap<>();
- for (ScoredNetwork network : networks) {
- List<ScoredNetwork> networkList = networksByType.get(network.networkKey.type);
- if (networkList == null) {
- networkList = new ArrayList<>();
- networksByType.put(network.networkKey.type, networkList);
- }
- networkList.add(network);
- }
-
- // Pass the scores of each type down to the appropriate network scorer.
- for (final Map.Entry<Integer, List<ScoredNetwork>> entry : networksByType.entrySet()) {
- final RemoteCallbackList<INetworkScoreCache> callbackList;
- final boolean isEmpty;
- synchronized (mScoreCaches) {
- callbackList = mScoreCaches.get(entry.getKey());
- isEmpty = callbackList == null || callbackList.getRegisteredCallbackCount() == 0;
- }
- if (isEmpty) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "No scorer registered for type " + entry.getKey() + ", discarding");
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // Separate networks by type.
+ Map<Integer, List<ScoredNetwork>> networksByType = new ArrayMap<>();
+ for (ScoredNetwork network : networks) {
+ List<ScoredNetwork> networkList = networksByType.get(network.networkKey.type);
+ if (networkList == null) {
+ networkList = new ArrayList<>();
+ networksByType.put(network.networkKey.type, networkList);
}
- continue;
+ networkList.add(network);
}
- sendCallback(new Consumer<INetworkScoreCache>() {
- @Override
- public void accept(INetworkScoreCache networkScoreCache) {
- try {
- networkScoreCache.updateScores(entry.getValue());
- } catch (RemoteException e) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Unable to update scores of type " + entry.getKey(), e);
+ // Pass the scores of each type down to the appropriate network scorer.
+ for (final Map.Entry<Integer, List<ScoredNetwork>> entry : networksByType.entrySet()) {
+ final RemoteCallbackList<INetworkScoreCache> callbackList;
+ final boolean isEmpty;
+ synchronized (mScoreCaches) {
+ callbackList = mScoreCaches.get(entry.getKey());
+ isEmpty = callbackList == null
+ || callbackList.getRegisteredCallbackCount() == 0;
+ }
+ if (isEmpty) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "No scorer registered for type " + entry.getKey()
+ + ", discarding");
+ }
+ continue;
+ }
+
+ sendCallback(new Consumer<INetworkScoreCache>() {
+ @Override
+ public void accept(INetworkScoreCache networkScoreCache) {
+ try {
+ networkScoreCache.updateScores(entry.getValue());
+ } catch (RemoteException e) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Unable to update scores of type " + entry.getKey(), e);
+ }
}
}
- }
- }, Collections.singleton(callbackList));
- }
+ }, Collections.singleton(callbackList));
+ }
- return true;
+ return true;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
@Override
@@ -369,8 +376,13 @@
if (mNetworkScorerAppManager.isCallerActiveScorer(getCallingUid()) ||
mContext.checkCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED) ==
PackageManager.PERMISSION_GRANTED) {
- clearInternal();
- return true;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ clearInternal();
+ return true;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
} else {
throw new SecurityException(
"Caller is neither the active scorer nor the scorer manager.");
@@ -428,35 +440,46 @@
INetworkScoreCache scoreCache,
int filterType) {
mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
- synchronized (mScoreCaches) {
- RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
- if (callbackList == null) {
- callbackList = new RemoteCallbackList<>();
- mScoreCaches.put(networkType, callbackList);
- }
- if (!callbackList.register(scoreCache, filterType)) {
- if (callbackList.getRegisteredCallbackCount() == 0) {
- mScoreCaches.remove(networkType);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mScoreCaches) {
+ RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
+ if (callbackList == null) {
+ callbackList = new RemoteCallbackList<>();
+ mScoreCaches.put(networkType, callbackList);
}
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Unable to register NetworkScoreCache for type " + networkType);
+ if (!callbackList.register(scoreCache, filterType)) {
+ if (callbackList.getRegisteredCallbackCount() == 0) {
+ mScoreCaches.remove(networkType);
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Unable to register NetworkScoreCache for type " + networkType);
+ }
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
}
@Override
public void unregisterNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
- synchronized (mScoreCaches) {
- RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
- if (callbackList == null || !callbackList.unregister(scoreCache)) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Unable to unregister NetworkScoreCache for type " + networkType);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mScoreCaches) {
+ RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
+ if (callbackList == null || !callbackList.unregister(scoreCache)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Unable to unregister NetworkScoreCache for type "
+ + networkType);
+ }
+ } else if (callbackList.getRegisteredCallbackCount() == 0) {
+ mScoreCaches.remove(networkType);
}
- } else if (callbackList.getRegisteredCallbackCount() == 0) {
- mScoreCaches.remove(networkType);
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
}
@@ -464,43 +487,53 @@
public RecommendationResult requestRecommendation(RecommendationRequest request) {
mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
throwIfCalledOnMainThread();
- final INetworkRecommendationProvider provider = getRecommendationProvider();
- if (provider != null) {
- try {
- return mRequestRecommendationCaller.getRecommendationResult(provider, request);
- } catch (RemoteException | TimeoutException e) {
- Log.w(TAG, "Failed to request a recommendation.", e);
- // TODO(jjoslin): 12/15/16 - Keep track of failures.
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final INetworkRecommendationProvider provider = getRecommendationProvider();
+ if (provider != null) {
+ try {
+ return mRequestRecommendationCaller.getRecommendationResult(provider, request);
+ } catch (RemoteException | TimeoutException e) {
+ Log.w(TAG, "Failed to request a recommendation.", e);
+ // TODO(jjoslin): 12/15/16 - Keep track of failures.
+ }
}
- }
- if (DBG) {
- Log.d(TAG, "Returning the default network recommendation.");
- }
+ if (DBG) {
+ Log.d(TAG, "Returning the default network recommendation.");
+ }
- WifiConfiguration selectedConfig = null;
- if (request != null) {
- selectedConfig = request.getCurrentSelectedConfig();
+ if (request != null && request.getCurrentSelectedConfig() != null) {
+ return RecommendationResult.createConnectRecommendation(
+ request.getCurrentSelectedConfig());
+ }
+ return RecommendationResult.createDoNotConnectRecommendation();
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- return new RecommendationResult(selectedConfig);
}
@Override
public boolean requestScores(NetworkKey[] networks) {
mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
- final INetworkRecommendationProvider provider = getRecommendationProvider();
- if (provider != null) {
- try {
- provider.requestScores(networks);
- // TODO(jjoslin): 12/15/16 - Consider pushing null scores into the cache to prevent
- // repeated requests for the same scores.
- return true;
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to request scores.", e);
- // TODO(jjoslin): 12/15/16 - Keep track of failures.
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final INetworkRecommendationProvider provider = getRecommendationProvider();
+ if (provider != null) {
+ try {
+ provider.requestScores(networks);
+ // TODO(jjoslin): 12/15/16 - Consider pushing null scores into the cache to
+ // prevent repeated requests for the same scores.
+ return true;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to request scores.", e);
+ // TODO(jjoslin): 12/15/16 - Keep track of failures.
+ }
}
+ return false;
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- return false;
}
@Override
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index c65aed7..11e1a9d 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -201,25 +201,9 @@
private final Map<Account, Map<String, String>> userDataCache = new HashMap<>();
/** protected by the {@link #cacheLock} */
private final Map<Account, Map<String, String>> authTokenCache = new HashMap<>();
-
/** protected by the {@link #cacheLock} */
private final TokenCache accountTokenCaches = new TokenCache();
- /** protected by the {@link #cacheLock} */
- private final Map<String, ArrayList<Integer>> mApplicationAccountRequestMappings =
- new HashMap<>();
-
- /* Together the below two Sparse Arrays serve as visible list. One maps UID to account
- number. Another maps Account number to Account.*/
-
- /** protected by the {@link #cacheLock} */
- private final SparseArray<ArrayList<Integer>> mVisibleListUidToMockAccountNumbers =
- new SparseArray<>();
-
- //TODO: Instead of using Mock Account IDs, use the actual account IDs.
- /** protected by the {@link #cacheLock} */
- private final SparseArray<Account> mMockAccountIdToAccount = new SparseArray<>();
-
/**
* protected by the {@link #cacheLock}
*
@@ -301,20 +285,7 @@
@Override
public void run() {
purgeOldGrantsAll();
-
- /* clears application request's for account types supported */
- int uidOfUninstalledApplication =
- intent.getIntExtra(Intent.EXTRA_UID, -1);
- if(uidOfUninstalledApplication != -1) {
- clearRequestedAccountVisibility(uidOfUninstalledApplication,
- getUserAccounts(UserHandle.getUserId(
- uidOfUninstalledApplication)));
- }
-
- /* removes visibility of previous UID of this uninstalled application*/
- removeAccountVisibilityAllAccounts(uidOfUninstalledApplication,
- getUserAccounts(UserHandle.getUserId(
- uidOfUninstalledApplication)));
+ // TODO remove visibility entries.
}
};
mHandler.post(purgingRunnable);
@@ -472,18 +443,20 @@
}
@Override
- public boolean addAccountExplicitlyWithUid(Account account, String password, Bundle extras,
- int[] selectedUids) {
- if(addAccountExplicitly(account,password,extras)) {
- for(int thisUid : selectedUids) {
- makeAccountVisible(account, thisUid);
- }
- return true;
- }
+ public boolean addAccountExplicitlyWithVisibility(Account account, String password, Bundle extras,
+ Map uidToVisibility) {
+ // TODO implementation
return false;
}
@Override
+ public Map<Account, Integer> getAccountsAndVisibilityForPackage(String packageName,
+ String accountType) {
+ // TODO Implement.
+ return new HashMap<Account, Integer>();
+ }
+
+ @Override
public int[] getRequestingUidsForType(String accountType) {
int callingUid = Binder.getCallingUid();
if (!isAccountManagedByCaller(accountType, callingUid, UserHandle.getUserId(callingUid))) {
@@ -493,204 +466,23 @@
accountType);
throw new SecurityException(msg);
}
- return getRequestingUidsForType(accountType, getUserAccounts(
- UserHandle.getUserId(callingUid)));
- }
-
- /**
- * Returns all UIDs for applications that requested the account type. This method
- * is called indirectly by the Authenticator and AccountManager
- *
- * @param accountType authenticator would like to know the requesting apps of
- * @param ua UserAccount that currently hosts the account and application
- *
- * @return ArrayList of all UIDs that support accounts of this
- * account type that seek approval (to be used to know which accounts for
- * the authenticator to include in addAccountExplicitly). Null if none.
- */
- private int[] getRequestingUidsForType(String accountType, UserAccounts ua) {
- synchronized(ua.cacheLock) {
- Map<String, ArrayList<Integer>> userApplicationAccountRequestMappings =
- ua.mApplicationAccountRequestMappings;
- ArrayList<Integer> allUidsForAccountType = userApplicationAccountRequestMappings.get(
- accountType);
- if(allUidsForAccountType == null) {
- return null;
- }
- int[] toReturn = new int[allUidsForAccountType.size()];
- for(int i = 0 ; i < toReturn.length ; i++) {
- toReturn[i] = allUidsForAccountType.get(i);
- }
- return toReturn;
- }
+ // TODO Implement.
+ return new int[]{};
}
@Override
- public boolean isAccountVisible(Account a, int uid) {
- int callingUid = Binder.getCallingUid();
- if (!isAccountManagedByCaller(a.type, callingUid, UserHandle.getUserId(callingUid))) {
- String msg = String.format(
- "uid %s cannot get secrets for accounts of type: %s",
- callingUid,
- a.type);
- throw new SecurityException(msg);
- }
- return isAccountVisible(a, uid, getUserAccounts(UserHandle.getUserId(callingUid)));
- }
-
- /**
- * Checks visibility of certain account of a process identified
- * by a given UID. This is called by the Authenticator indirectly.
- *
- * @param a The account to check visibility of
- * @param uid UID to check visibility of
- * @param ua UserAccount that currently hosts the account and application
- *
- * @return True if application has access to the account
- *
- */
- private boolean isAccountVisible(Account a, int uid, UserAccounts ua) {
- if(isAccountManagedByCaller(a.type, uid, UserHandle.getUserId(uid))) {
- return true;
- }
- int accountMapping = getMockAccountNumber(a, ua);
- if(accountMapping < 0) {
- return true;
- }
- synchronized(ua.cacheLock) {
- SparseArray<Account> userAcctIdToAcctMap = ua.mMockAccountIdToAccount;
- SparseArray<ArrayList<Integer>> userWlUidToMockAccountNums =
- ua.mVisibleListUidToMockAccountNumbers;
- ArrayList<Integer> linkedAccountsToUid = userWlUidToMockAccountNums.get(uid);
- int indexOfAccountMapping = userAcctIdToAcctMap.indexOfValueByValue(a);
- return indexOfAccountMapping == -1 || (linkedAccountsToUid != null
- && linkedAccountsToUid.contains(accountMapping));
- }
+ public int getAccountVisibility(Account a, int uid) {
+ // TODO Implement.
+ return 0;
}
@Override
- public boolean makeAccountVisible(Account a, int uid) {
- int callingUid = Binder.getCallingUid();
- if (!isAccountManagedByCaller(a.type, callingUid, UserHandle.getUserId(callingUid))) {
- String msg = String.format(
- "uid %s cannot get secrets for accounts of type: %s",
- callingUid,
- a.type);
- throw new SecurityException(msg);
- }
- return makeAccountVisible(a, uid, getUserAccounts(UserHandle.getUserId(callingUid)));
- }
-
- /**
- * Gives a certain UID, represented a application, access to an account. This method
- * is called indirectly by the Authenticator.
- *
- * @param a Account to make visible
- * @param uid to add visibility of the Account from
- * @param ua UserAccount that currently hosts the account and application
- *
- * @return True if account made visible to application and was not previously visible.
- */
- private boolean makeAccountVisible(Account a, int uid, UserAccounts ua) {
- int accountMapping = getMockAccountNumber(a, ua);
- if(accountMapping < 0) {
- accountMapping = makeAccountNumber(a, ua);
- }
- synchronized(ua.cacheLock) {
- SparseArray<ArrayList<Integer>> userWlUidToMockAccountNums =
- ua.mVisibleListUidToMockAccountNumbers;
- ArrayList<Integer> linkedAccountsToUid = userWlUidToMockAccountNums.get(uid);
- if(linkedAccountsToUid == null) {
- linkedAccountsToUid = new ArrayList<>();
- linkedAccountsToUid.add(accountMapping);
- userWlUidToMockAccountNums.put(uid, linkedAccountsToUid);
- } else if(!linkedAccountsToUid.contains(accountMapping)) {
- linkedAccountsToUid.add(accountMapping);
- } else {
- return false;
- }
- }
-
- String[] subPackages = mPackageManager.getPackagesForUid(uid);
- if(subPackages != null) {
- for(String subPackage : subPackages) {
- sendNotification(subPackage, a);
- }
- }
- return true;
- }
-
- @Override
- public boolean removeAccountVisibility(Account a, int uid) {
- int callingUid = Binder.getCallingUid();
- if (!isAccountManagedByCaller(a.type, callingUid, UserHandle.getUserId(callingUid))) {
- String msg = String.format(
- "uid %s cannot get secrets for accounts of type: %s",
- callingUid,
- a.type);
- throw new SecurityException(msg);
- }
- return removeAccountVisibility(a, uid, getUserAccounts(UserHandle.getUserId(callingUid)));
- }
-
- /**
- * Removes visibility of certain account of a process identified
- * by a given UID to an application. This is called directly by the
- * AccountManager and indirectly by the Authenticator.
- *
- * @param a Account to remove visibility from
- * @param uid UID to remove visibility of the Account from
- * @param ua UserAccount that hosts the account and application
- *
- * @return True if application access to account removed and was previously visible.
- */
- private boolean removeAccountVisibility(Account a, int uid, UserAccounts ua) {
- int accountMapping = getMockAccountNumber(a, ua);
- if(accountMapping < 0) {
- return false;
- }
- synchronized(ua.cacheLock) {
- SparseArray<ArrayList<Integer>> userWlUidToMockAccountNums =
- ua.mVisibleListUidToMockAccountNumbers;
- ArrayList<Integer> linkedAccountsToUid = userWlUidToMockAccountNums.get(uid);
- if(linkedAccountsToUid != null) {
- boolean toReturn = linkedAccountsToUid.remove((Integer) accountMapping);
- if(linkedAccountsToUid.size() == 0) {
- userWlUidToMockAccountNums.remove(uid);
- }
- return toReturn;
- }
- }
+ public boolean setAccountVisibility(Account a, int uid, int visibility) {
+ // TODO Implement.
return false;
}
/**
- * Registers an application's preferences for supported account types for login. This is
- * a helper method of requestAccountVisibility and indirectly called by AccountManager.
- *
- * @param accountTypes account types third party app is willing to support
- * @param uid of application requesting account visibility
- * @param ua UserAccount that hosts the account and application
- */
- private void addRequestedAccountsVisibility(String[] accountTypes, int uid, UserAccounts ua) {
- synchronized(ua.cacheLock) {
- Map<String, ArrayList<Integer>> userApplicationAccountRequestMappings =
- ua.mApplicationAccountRequestMappings;
- for(String accountType : accountTypes) {
- ArrayList<Integer> accountUidAppList = userApplicationAccountRequestMappings
- .get(accountType);
- if(accountUidAppList == null) {
- accountUidAppList = new ArrayList<>();
- accountUidAppList.add(uid);
- userApplicationAccountRequestMappings.put(accountType, accountUidAppList);
- } else if (!accountUidAppList.contains(uid)) {
- accountUidAppList.add(uid);
- }
- }
- }
- }
-
- /**
* Registers the requested login account types requested by all the applications already
* installed on the device.
*/
@@ -706,124 +498,8 @@
}
/**
- * Clears all preferences an application had for login account types it offered
- * support for. This method is used by AccountManager after application is
- * uninstalled.
- *
- * @param uid Uid of the application to clear account type preferences
- * @param ua UserAccount that hosted the account and application
- *
- * @return true if any previous settings were overridden.
- */
- private boolean clearRequestedAccountVisibility(int uid, UserAccounts ua) {
- boolean accountsDeleted = false;
- ArrayList<String> accountTypesToRemove = new ArrayList<>();
- synchronized(ua.cacheLock) {
- Map<String, ArrayList<Integer>> userApplicationAccountRequestMappings =
- ua.mApplicationAccountRequestMappings;
- Set<Entry<String, ArrayList<Integer>>> accountTypeAppListEntries =
- userApplicationAccountRequestMappings.entrySet();
-
- for(Entry<String, ArrayList<Integer>> entry : accountTypeAppListEntries) {
- ArrayList<Integer> supportedApps = entry.getValue();
- if(supportedApps.remove((Integer) uid)) {
- accountsDeleted = true;
- }
-
- if(supportedApps.isEmpty()) {
- accountTypesToRemove.add(entry.getKey());
- }
- }
-
- for(String s : accountTypesToRemove) {
- userApplicationAccountRequestMappings.remove(s);
- }
- }
-
- return accountsDeleted;
- }
-
- /**
- * Retrieves the mock account number associated with an Account in order to later retrieve
- * the account from the Integer-Account Mapping. An account number is not the same as
- * accountId in the database. This method can be indirectly called by AccountManager and
- * indirectly by the Authenticator.
- *
- * @param a account to retrieve account number mapping
- * @param ua UserAccount that currently hosts the account and application
- *
- * @return account number affiliated with the Account in question. Negative number if none.
- */
- private int getMockAccountNumber(Account a, UserAccounts ua) {
- //TODO: Each account is linked to AccountId rather than generated mock account numbers
- SparseArray<Account> userAcctIdToAcctMap =
- ua.mMockAccountIdToAccount;
- synchronized(ua.cacheLock) {
- int indexOfAccount = userAcctIdToAcctMap.indexOfValueByValue(a);
- if(indexOfAccount < 0) {
- return -1;
- }
- return userAcctIdToAcctMap.keyAt(indexOfAccount);
- }
- }
-
- /**
- * Returns a full list of accounts that a certain UID is allowed access
- * based on the visible list entries.
- *
- * @param uid of application to retrieve visible listed accounts for
- * @param ua UserAccount that currently hosts the account and application
- *
- * @return array of Account values that are accessible by the given uids
- */
- private Account[] getVisibleListedAccounts(int uid, UserAccounts ua) {
- ArrayList<Account> visibleListedAccounts = new ArrayList<>();
- synchronized(ua.cacheLock) {
- SparseArray<Account> userAcctIdToAcctMap = ua.mMockAccountIdToAccount;
- SparseArray<ArrayList<Integer>> userWlUidToMockAccountNums =
- ua.mVisibleListUidToMockAccountNumbers;
- ArrayList<Integer> visibleListedUidAccountNumbers =
- userWlUidToMockAccountNums.get(uid);
- if(visibleListedUidAccountNumbers != null) {
- for(Integer accountNumber : visibleListedUidAccountNumbers) {
- Account currentAccount = userAcctIdToAcctMap.get(accountNumber);
- visibleListedAccounts.add(currentAccount);
- }
- }
- }
- Account[] arrVisibleListedAccounts = new Account[visibleListedAccounts.size()];
- return visibleListedAccounts.toArray(arrVisibleListedAccounts);
- }
-
- /**
- * Makes an account number for a given Account to be mapped to.
- * This method is called by makeVisible if an Account does not have
- * a mapping for the visible list. This method is thus indirectly
- * called by the Authenticator.
- *
- * @param a account to make an account number mapping of
- * @param ua UserAccount that currently hosts the account and application
- *
- * @return account number created to map to the given account
- */
- // TODO: Remove this method and use accountId from DB.
- private int makeAccountNumber(Account a, UserAccounts ua) {
- synchronized(ua.cacheLock) {
- SparseArray<Account> userAcctIdToAcctMap = ua.mMockAccountIdToAccount;
- int newAccountMapping = 0;
- while(userAcctIdToAcctMap.get(newAccountMapping) != null) {
- newAccountMapping++;
- }
- userAcctIdToAcctMap.put(newAccountMapping, a);
- return newAccountMapping;
- }
- }
-
-
-
- /**
- * Registers an application, represented by a UID, to support account types detailed in
- * the applications manifest as well as allowing it to opt for notifications.
+ * Registers an application, represented by a UID, to support account types detailed in the
+ * applications manifest as well as allowing it to opt for notifications.
*
* @param uid UID of application
* @param ua UserAccount that currently hosts the account and application
@@ -834,105 +510,27 @@
try {
String[] allPackages = mPackageManager.getPackagesForUid(uid);
if (allPackages != null) {
- for(String aPackage : allPackages) {
+ for (String aPackage : allPackages) {
ApplicationInfo ai = mPackageManager.getApplicationInfo(aPackage,
PackageManager.GET_META_DATA);
Bundle b = ai.metaData;
- if(b == null) {
+ if (b == null) {
return;
}
- interestedPackages = b.getString("android.accounts.SupportedLoginTypes");
+ interestedPackages = b.getString(AccountManager.SUPPORTED_ACCOUNT_TYPES);
}
}
} catch (PackageManager.NameNotFoundException e) {
Log.d("NameNotFoundException", e.getMessage());
}
- if(interestedPackages != null) {
- /* request remote account types directly from here. Reads from Android Manifest */
- requestAccountVisibility(interestedPackages.split(";"), uid, ua);
+ if (interestedPackages != null) {
+ // TODO request visibility
+ // requestAccountVisibility(interestedPackages.split(";"), uid, ua);
}
}
/**
- * Allows AccountManager to register account types that an application has login
- * support for. This method over-writes all of the application's previous settings
- * for accounts it supported.
- *
- * @param accountTypes array of account types application wishes to support
- * @param uid of application registering requested account types
- * @param ua UserAccount that hosts the account and application
- */
- private void requestAccountVisibility(String[] accountTypes, int uid, UserAccounts ua) {
- if(accountTypes.length > 0) {
- clearRequestedAccountVisibility(uid, ua);
- addRequestedAccountsVisibility(accountTypes, uid, ua);
- }
- }
-
- /**
- * Removes visibility of all Accounts to this particular UID. This is called when an
- * application is uninstalled so another application that is installed with the same
- * UID cannot access Accounts. This is called by AccountManager.
- *
- * @param uid of application to remove all Account visibility to
- * @param ua UserAccount that hosts the current Account
- */
- private void removeAccountVisibilityAllAccounts(int uid, UserAccounts ua) {
- synchronized(ua.cacheLock) {
- SparseArray<ArrayList<Integer>> userWlUidToMockAccountNums =
- ua.mVisibleListUidToMockAccountNumbers;
- SparseArray<Account> userAcctIdToAcctMap = ua.mMockAccountIdToAccount;
- ArrayList<Integer> allAccountNumbersList = userWlUidToMockAccountNums.get(uid);
- if(allAccountNumbersList != null) {
- Integer[] allAccountNumbers = allAccountNumbersList.toArray(
- new Integer[allAccountNumbersList.size()]);
- for(int accountNum : allAccountNumbers) {
- removeAccountVisibility(userAcctIdToAcctMap.get(accountNum), uid, ua);
- }
- }
- }
- }
-
- /**
- * Removes visible list functionality of a certain Account.
- * This method is currently called by (1) addAccountExplicitly (as opposed to
- * addAccountExplicitlyWithUid) and (2) removeAccountExplicitly.
- *
- * @param a the account to clear the visible list functionality for
- * @param ua currently UserAccounts profile containing Account
- *
- * @return true if account previously had visible list functionality
- */
- private boolean removeVisibleListFunctionality(Account a, UserAccounts ua) {
- int mockAccountNum = getMockAccountNumber(a, ua);
- if(mockAccountNum < 0) {
- return false;
- }
- synchronized(ua.cacheLock) {
- SparseArray<ArrayList<Integer>> userWlUidToMockAccountNums =
- ua.mVisibleListUidToMockAccountNumbers;
- SparseArray<Account> userAcctIdToAcctMap = ua.mMockAccountIdToAccount;
-
- /* Removing mapping from account number to account removes visible list functionality*/
- userAcctIdToAcctMap.remove(mockAccountNum);
-
- for(int i = userWlUidToMockAccountNums.size() - 1 ; i >= 0 ; i--) {
- int uidKey = userWlUidToMockAccountNums.keyAt(i);
- ArrayList<Integer> allAccountNumbers = userWlUidToMockAccountNums.get(uidKey);
- if(allAccountNumbers != null) {
- allAccountNumbers.remove(mockAccountNum);
- if(allAccountNumbers.isEmpty()) {
- userWlUidToMockAccountNums.remove(uidKey);
- }
- }
- }
- }
- return true;
- }
-
- /**
- * Sends a direct intent to a package, notifying it of a visible account. This
- * method is a helper method of makeAccountVisible.
+ * Sends a direct intent to a package, notifying it of a visible account change.
*
* @param desiredPackage to send Account to
* @param visibleAccount to send to package
@@ -940,9 +538,10 @@
private void sendNotification(String desiredPackage, Account visibleAccount) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
- intent.setAction(NEW_ACCOUNT_VISIBLE);
+ intent.setAction(AccountManager.ACTION_VISIBLE_ACCOUNTS_CHANGED);
intent.setPackage(desiredPackage);
- intent.putExtra("android.accounts.KEY_ACCOUNT", (Account) visibleAccount);
+ // TODO update documentation, add account extra if new account became visible
+ // intent.putExtra("android.accounts.KEY_ACCOUNT", (Account) visibleAccount);
mContext.sendBroadcast(intent);
}
@@ -1471,7 +1070,7 @@
account.type);
throw new SecurityException(msg);
}
- removeVisibleListFunctionality(account, getUserAccounts(UserHandle.getUserId(callingUid)));
+
/*
* Child users are not allowed to add accounts. Only the accounts that are
* shared by the parent profile can be added to child profile.
@@ -2050,7 +1649,6 @@
account.type);
throw new SecurityException(msg);
}
- removeVisibleListFunctionality(account, getUserAccounts(UserHandle.getUserId(callingUid)));
UserAccounts accounts = getUserAccountsForCaller();
final long accountId = accounts.accountsDb.findDeAccountId(account);
logRecord(
@@ -3303,7 +2901,7 @@
throw new IllegalArgumentException("sessionBundle is empty");
}
- // Only allow the system process to finish session for other users
+ // Only allow the system process to finish session for other users.
if (isCrossUser(callingUid, userId)) {
throw new SecurityException(
String.format(
@@ -4095,23 +3693,11 @@
long identityToken = clearCallingIdentity();
try {
UserAccounts accounts = getUserAccounts(userId);
- Account[] accountsToReturn = getAccountsInternal(
+ return getAccountsInternal(
accounts,
callingUid,
callingPackage,
visibleAccountTypes);
- ArrayList<Account> accountsToReturnList = new
- ArrayList<Account>(Arrays.asList(accountsToReturn));
- for(int i = accountsToReturnList.size() - 1; i >= 0 ; i--) {
- // if account not visible to caller or managed by caller, remove from
- // accounts to return. Note that all accounts visible by default unless
- // visible list functionality implemented
- if(!(isAccountVisible(accountsToReturnList.get(i), callingUid,
- getUserAccounts(userId)))) {
- accountsToReturnList.remove(i);
- }
- }
- return accountsToReturnList.toArray(new Account[accountsToReturnList.size()]);
} finally {
restoreCallingIdentity(identityToken);
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index 461a9b0..4a10f50 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -29,9 +29,12 @@
import com.android.server.hdmi.HdmiAnnotations.IoThreadOnly;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Date;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
import libcore.util.EmptyArray;
import sun.util.locale.LanguageTag;
@@ -69,6 +72,8 @@
private static final int NUM_LOGICAL_ADDRESS = 16;
+ private static final int MAX_CEC_MESSAGE_HISTORY = 20;
+
// Predicate for whether the given logical address is remote device's one or not.
private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() {
@Override
@@ -101,6 +106,10 @@
// Stores the local CEC devices in the system. Device type is used for key.
private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
+ // Stores recent CEC messages history for debugging purpose.
+ private final ArrayBlockingQueue<MessageHistoryRecord> mMessageHistory =
+ new ArrayBlockingQueue<>(MAX_CEC_MESSAGE_HISTORY);
+
// Private constructor. Use HdmiCecController.create().
private HdmiCecController(HdmiControlService service) {
mService = service;
@@ -580,6 +589,7 @@
void sendCommand(final HdmiCecMessage cecMessage,
final HdmiControlService.SendMessageCallback callback) {
assertRunOnServiceThread();
+ addMessageToHistory(false /* isReceived */, cecMessage);
runOnIoThread(new Runnable() {
@Override
public void run() {
@@ -597,7 +607,7 @@
final int finalError = errorCode;
if (finalError != SendMessageResult.SUCCESS) {
- Slog.w(TAG, "Failed to send " + cecMessage);
+ Slog.w(TAG, "Failed to send " + cecMessage + " with errorCode=" + finalError);
}
if (callback != null) {
runOnServiceThread(new Runnable() {
@@ -619,6 +629,7 @@
assertRunOnServiceThread();
HdmiCecMessage command = HdmiCecMessageBuilder.of(srcAddress, dstAddress, body);
HdmiLogger.debug("[R]:" + command);
+ addMessageToHistory(true /* isReceived */, command);
onReceiveCommand(command);
}
@@ -632,6 +643,16 @@
mService.onHotplug(port, connected);
}
+ @ServiceThreadOnly
+ private void addMessageToHistory(boolean isReceived, HdmiCecMessage message) {
+ assertRunOnServiceThread();
+ MessageHistoryRecord record = new MessageHistoryRecord(isReceived, message);
+ if (!mMessageHistory.offer(record)) {
+ mMessageHistory.poll();
+ mMessageHistory.offer(record);
+ }
+ }
+
void dump(final IndentingPrintWriter pw) {
for (int i = 0; i < mLocalDevices.size(); ++i) {
pw.println("HdmiCecLocalDevice #" + i + ":");
@@ -639,6 +660,13 @@
mLocalDevices.valueAt(i).dump(pw);
pw.decreaseIndent();
}
+ pw.println("CEC message history:");
+ pw.increaseIndent();
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ for (MessageHistoryRecord record : mMessageHistory) {
+ record.dump(pw, sdf);
+ }
+ pw.decreaseIndent();
}
private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
@@ -654,4 +682,24 @@
private static native void nativeSetLanguage(long controllerPtr, String language);
private static native void nativeEnableAudioReturnChannel(long controllerPtr, int port, boolean flag);
private static native boolean nativeIsConnected(long controllerPtr, int port);
+
+ private final class MessageHistoryRecord {
+ private final long mTime;
+ private final boolean mIsReceived; // true if received message and false if sent message
+ private final HdmiCecMessage mMessage;
+
+ public MessageHistoryRecord(boolean isReceived, HdmiCecMessage message) {
+ mTime = System.currentTimeMillis();
+ mIsReceived = isReceived;
+ mMessage = message;
+ }
+
+ void dump(final IndentingPrintWriter pw, SimpleDateFormat sdf) {
+ pw.print(mIsReceived ? "[R]" : "[S]");
+ pw.print(" time=");
+ pw.print(sdf.format(new Date(mTime)));
+ pw.print(" message=");
+ pw.println(mMessage);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/pm/AbstractStatsBase.java b/services/core/java/com/android/server/pm/AbstractStatsBase.java
index 612c476..0053f58 100644
--- a/services/core/java/com/android/server/pm/AbstractStatsBase.java
+++ b/services/core/java/com/android/server/pm/AbstractStatsBase.java
@@ -60,12 +60,12 @@
return new AtomicFile(fname);
}
- void writeNow(final T data) {
+ protected void writeNow(final T data) {
writeImpl(data);
mLastTimeWritten.set(SystemClock.elapsedRealtime());
}
- boolean maybeWriteAsync(final T data) {
+ protected boolean maybeWriteAsync(final T data) {
if (SystemClock.elapsedRealtime() - mLastTimeWritten.get() < WRITE_INTERVAL_MS
&& !PackageManagerService.DEBUG_DEXOPT) {
return false;
@@ -105,7 +105,7 @@
protected abstract void writeInternal(T data);
- void read(T data) {
+ protected void read(T data) {
if (mLock) {
synchronized (data) {
synchronized (mFileLock) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 58783ad..ec4b7eb 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -260,6 +260,7 @@
import com.android.server.pm.PermissionsState.PermissionState;
import com.android.server.pm.Settings.DatabaseVersion;
import com.android.server.pm.Settings.VersionInfo;
+import com.android.server.pm.dex.DexManager;
import com.android.server.storage.DeviceStorageMonitorInternal;
import dalvik.system.CloseGuard;
@@ -303,6 +304,7 @@
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -748,6 +750,9 @@
final PackageInstallerService mInstallerService;
private final PackageDexOptimizer mPackageDexOptimizer;
+ // DexManager handles the usage of dex files (e.g. secondary files, whether or not a package
+ // is used by other apps).
+ private final DexManager mDexManager;
private AtomicInteger mNextMoveId = new AtomicInteger();
private final MoveCallbacks mMoveCallbacks;
@@ -2165,6 +2170,7 @@
mInstaller = installer;
mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
"*dexopt*");
+ mDexManager = new DexManager();
mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
mOnPermissionChangeListeners = new OnPermissionChangeListeners(
@@ -2616,6 +2622,19 @@
mPackageUsage.read(mPackages);
mCompilerStats.read();
+ // Read and update the usage of dex files.
+ // At this point we know the code paths of the packages, so we can validate
+ // the disk file and build the internal cache.
+ // The usage file is expected to be small so loading and verifying it
+ // should take a fairly small time compare to the other activities (e.g. package
+ // scanning).
+ final Map<Integer, List<PackageInfo>> userPackages = new HashMap<>();
+ final int[] currentUserIds = UserManagerService.getInstance().getUserIds();
+ for (int userId : currentUserIds) {
+ userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList());
+ }
+ mDexManager.load(userPackages);
+
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END,
SystemClock.uptimeMillis());
Slog.i(TAG, "Time to scan packages: "
@@ -7641,7 +7660,14 @@
@Override
public void notifyDexLoad(String loadingPackageName, List<String> dexPaths, String loaderIsa) {
- // TODO(calin): b/32871170
+ int userId = UserHandle.getCallingUserId();
+ ApplicationInfo ai = getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
+ if (ai == null) {
+ Slog.w(TAG, "Loading a package that does not exist for the calling user. package="
+ + loadingPackageName + ", user=" + userId);
+ return;
+ }
+ mDexManager.notifyDexLoad(ai, dexPaths, loaderIsa, userId);
}
// TODO: this is not used nor needed. Delete it.
@@ -21616,6 +21642,10 @@
PackageManagerService.this.requestEphemeralResolutionPhaseTwo(
responseObj, origIntent, resolvedType, launchIntent, callingPackage, userId);
}
+
+ public String getSetupWizardPackageName() {
+ return mSetupWizardPackage;
+ }
}
@Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index cfd0af7..45887e1 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -24,11 +24,13 @@
import android.content.Intent;
import android.content.pm.PackageParser;
import android.content.pm.ResolveInfo;
+import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
import android.system.ErrnoException;
import android.util.ArraySet;
import android.util.Log;
+import dalvik.system.VMRuntime;
import libcore.io.Libcore;
import java.io.File;
@@ -197,4 +199,17 @@
}
return sb.toString();
}
+
+ /**
+ * Verifies that the given string {@code isa} is a valid supported isa on
+ * the running device.
+ */
+ public static boolean checkISA(String isa) {
+ for (String abi : Build.SUPPORTED_ABIS) {
+ if (VMRuntime.getInstructionSet(abi).equals(isa)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
new file mode 100644
index 0000000..aa2bcef
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2016 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.dex;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageParser;
+import android.content.pm.ApplicationInfo;
+
+import android.util.Slog;
+
+import com.android.server.pm.PackageManagerServiceUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class keeps track of how dex files are used.
+ * Every time it gets a notification about a dex file being loaded it tracks
+ * its owning package and records it in PackageDexUsage (package-dex-usage.list).
+ *
+ * TODO(calin): Extract related dexopt functionality from PackageManagerService
+ * into this class.
+ */
+public class DexManager {
+ private static final String TAG = "DexManager";
+
+ private static final boolean DEBUG = false;
+
+ // Maps package name to code locations.
+ // It caches the code locations for the installed packages. This allows for
+ // faster lookups (no locks) when finding what package owns the dex file.
+ private final Map<String, PackageCodeLocations> mPackageCodeLocationsCache;
+
+ // PackageDexUsage handles the actual I/O operations. It is responsible to
+ // encode and save the dex usage data.
+ private final PackageDexUsage mPackageDexUsage;
+
+ // Possible outcomes of a dex search.
+ private static int DEX_SEARCH_NOT_FOUND = 0; // dex file not found
+ private static int DEX_SEARCH_FOUND_PRIMARY = 1; // dex file is the primary/base apk
+ private static int DEX_SEARCH_FOUND_SPLIT = 2; // dex file is a split apk
+ private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex
+
+ public DexManager() {
+ mPackageCodeLocationsCache = new HashMap<>();
+ mPackageDexUsage = new PackageDexUsage();
+ }
+
+ /**
+ * Notify about dex files loads.
+ * Note that this method is invoked when apps load dex files and it should
+ * return as fast as possible.
+ *
+ * @param loadingPackage the package performing the load
+ * @param dexPaths the list of dex files being loaded
+ * @param loaderIsa the ISA of the app loading the dex files
+ * @param loaderUserId the user id which runs the code loading the dex files
+ */
+ public void notifyDexLoad(ApplicationInfo loadingAppInfo, List<String> dexPaths,
+ String loaderIsa, int loaderUserId) {
+ try {
+ notifyDexLoadInternal(loadingAppInfo, dexPaths, loaderIsa, loaderUserId);
+ } catch (Exception e) {
+ Slog.w(TAG, "Exception while notifying dex load for package " +
+ loadingAppInfo.packageName, e);
+ }
+ }
+
+ private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo, List<String> dexPaths,
+ String loaderIsa, int loaderUserId) {
+ if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
+ Slog.w(TAG, "Loading dex files " + dexPaths + " in unsupported ISA: " +
+ loaderIsa + "?");
+ return;
+ }
+
+ for (String dexPath : dexPaths) {
+ // Find the owning package name.
+ DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId);
+
+ if (DEBUG) {
+ Slog.i(TAG, loadingAppInfo.packageName
+ + " loads from " + searchResult + " : " + loaderUserId + " : " + dexPath);
+ }
+
+ if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) {
+ // TODO(calin): extend isUsedByOtherApps check to detect the cases where
+ // different apps share the same runtime. In that case we should not mark the dex
+ // file as isUsedByOtherApps. Currently this is a safe approximation.
+ boolean isUsedByOtherApps = !loadingAppInfo.packageName.equals(
+ searchResult.mOwningPackageName);
+ boolean primaryOrSplit = searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY ||
+ searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT;
+
+ if (primaryOrSplit && !isUsedByOtherApps) {
+ // If the dex file is the primary apk (or a split) and not isUsedByOtherApps
+ // do not record it. This case does not bring any new usable information
+ // and can be safely skipped.
+ continue;
+ }
+
+ // Record dex file usage. If the current usage is a new pattern (e.g. new secondary,
+ // or UsedBytOtherApps), record will return true and we trigger an async write
+ // to disk to make sure we don't loose the data in case of a reboot.
+ if (mPackageDexUsage.record(searchResult.mOwningPackageName,
+ dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
+ } else {
+ // This can happen in a few situations:
+ // - bogus dex loads
+ // - recent installs/uninstalls that we didn't detect.
+ // - new installed splits
+ // If we can't find the owner of the dex we simply do not track it. The impact is
+ // that the dex file will not be considered for offline optimizations.
+ // TODO(calin): add hooks for install/uninstall notifications to
+ // capture new or obsolete packages.
+ if (DEBUG) {
+ Slog.i(TAG, "Could not find owning package for dex file: " + dexPath);
+ }
+ }
+ }
+ }
+
+ /**
+ * Read the dex usage from disk and populate the code cache locations.
+ * @param existingPackages a map containing information about what packages
+ * are available to what users. Only packages in this list will be
+ * recognized during notifyDexLoad().
+ */
+ public void load(Map<Integer, List<PackageInfo>> existingPackages) {
+ try {
+ loadInternal(existingPackages);
+ } catch (Exception e) {
+ mPackageDexUsage.clear();
+ Slog.w(TAG, "Exception while loading package dex usage. " +
+ "Starting with a fresh state.", e);
+ }
+ }
+
+ private void loadInternal(Map<Integer, List<PackageInfo>> existingPackages) {
+ Map<String, Set<Integer>> packageToUsersMap = new HashMap<>();
+ // Cache the code locations for the installed packages. This allows for
+ // faster lookups (no locks) when finding what package owns the dex file.
+ for (Map.Entry<Integer, List<PackageInfo>> entry : existingPackages.entrySet()) {
+ List<PackageInfo> packageInfoList = entry.getValue();
+ int userId = entry.getKey();
+ for (PackageInfo pi : packageInfoList) {
+ // Cache the code locations.
+ PackageCodeLocations pcl = mPackageCodeLocationsCache.get(pi.packageName);
+ if (pcl != null) {
+ pcl.mergeAppDataDirs(pi.applicationInfo, userId);
+ } else {
+ mPackageCodeLocationsCache.put(pi.packageName,
+ new PackageCodeLocations(pi.applicationInfo, userId));
+ }
+ // Cache a map from package name to the set of user ids who installed the package.
+ // We will use it to sync the data and remove obsolete entries from
+ // mPackageDexUsage.
+ Set<Integer> users = putIfAbsent(
+ packageToUsersMap, pi.packageName, new HashSet<>());
+ users.add(userId);
+ }
+ }
+
+ mPackageDexUsage.read();
+ mPackageDexUsage.syncData(packageToUsersMap);
+ }
+
+ /**
+ * Get the package dex usage for the given package name.
+ * @return the package data or null if there is no data available for this package.
+ */
+ public PackageDexUsage.PackageUseInfo getPackageUseInfo(String packageName) {
+ return mPackageDexUsage.getPackageUseInfo(packageName);
+ }
+
+ /**
+ * Retrieves the package which owns the given dexPath.
+ */
+ private DexSearchResult getDexPackage(
+ ApplicationInfo loadingAppInfo, String dexPath, int userId) {
+ // Ignore framework code.
+ // TODO(calin): is there a better way to detect it?
+ if (dexPath.startsWith("/system/framework/")) {
+ new DexSearchResult("framework", DEX_SEARCH_NOT_FOUND);
+ }
+
+ // First, check if the package which loads the dex file actually owns it.
+ // Most of the time this will be true and we can return early.
+ PackageCodeLocations loadingPackageCodeLocations =
+ new PackageCodeLocations(loadingAppInfo, userId);
+ int outcome = loadingPackageCodeLocations.searchDex(dexPath, userId);
+ if (outcome != DEX_SEARCH_NOT_FOUND) {
+ // TODO(calin): evaluate if we bother to detect symlinks at the dexPath level.
+ return new DexSearchResult(loadingPackageCodeLocations.mPackageName, outcome);
+ }
+
+ // The loadingPackage does not own the dex file.
+ // Perform a reverse look-up in the cache to detect if any package has ownership.
+ // Note that we can have false negatives if the cache falls out of date.
+ for (PackageCodeLocations pcl : mPackageCodeLocationsCache.values()) {
+ outcome = pcl.searchDex(dexPath, userId);
+ if (outcome != DEX_SEARCH_NOT_FOUND) {
+ return new DexSearchResult(pcl.mPackageName, outcome);
+ }
+ }
+
+ // Cache miss. Return not found for the moment.
+ //
+ // TODO(calin): this may be because of a newly installed package, an update
+ // or a new added user. We can either perform a full look up again or register
+ // observers to be notified of package/user updates.
+ return new DexSearchResult(null, DEX_SEARCH_NOT_FOUND);
+ }
+
+ private static <K,V> V putIfAbsent(Map<K,V> map, K key, V newValue) {
+ V existingValue = map.putIfAbsent(key, newValue);
+ return existingValue == null ? newValue : existingValue;
+ }
+
+ /**
+ * Convenience class to store the different locations where a package might
+ * own code.
+ */
+ private static class PackageCodeLocations {
+ private final String mPackageName;
+ private final String mBaseCodePath;
+ private final Set<String> mSplitCodePaths;
+ // Maps user id to the application private directory.
+ private final Map<Integer, Set<String>> mAppDataDirs;
+
+ public PackageCodeLocations(ApplicationInfo ai, int userId) {
+ mPackageName = ai.packageName;
+ mBaseCodePath = ai.sourceDir;
+ mSplitCodePaths = new HashSet<>();
+ if (ai.splitSourceDirs != null) {
+ for (String split : ai.splitSourceDirs) {
+ mSplitCodePaths.add(split);
+ }
+ }
+ mAppDataDirs = new HashMap<>();
+ mergeAppDataDirs(ai, userId);
+ }
+
+ public void mergeAppDataDirs(ApplicationInfo ai, int userId) {
+ Set<String> dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>());
+ dataDirs.add(ai.dataDir);
+
+ // Compute and cache the real path as well since data dir may be a symlink.
+ // e.g. /data/data/ -> /data/user/0/
+ try {
+ dataDirs.add(PackageManagerServiceUtils.realpath(new File(ai.dataDir)));
+ } catch (IOException e) {
+ Slog.w(TAG, "Error to get realpath of " + ai.dataDir, e);
+ }
+
+ }
+
+ public int searchDex(String dexPath, int userId) {
+ // First check that this package is installed or active for the given user.
+ // If we don't have a data dir it means this user is trying to load something
+ // unavailable for them.
+ Set<String> userDataDirs = mAppDataDirs.get(userId);
+ if (userDataDirs == null) {
+ Slog.w(TAG, "Trying to load a dex path which does not exist for the current " +
+ "user. dexPath=" + dexPath + ", userId=" + userId);
+ return DEX_SEARCH_NOT_FOUND;
+ }
+
+ if (mBaseCodePath.equals(dexPath)) {
+ return DEX_SEARCH_FOUND_PRIMARY;
+ }
+ if (mSplitCodePaths.contains(dexPath)) {
+ return DEX_SEARCH_FOUND_SPLIT;
+ }
+ for (String dataDir : userDataDirs) {
+ if (dexPath.startsWith(dataDir)) {
+ return DEX_SEARCH_FOUND_SECONDARY;
+ }
+ }
+ return DEX_SEARCH_NOT_FOUND;
+ }
+ }
+
+ /**
+ * Convenience class to store ownership search results.
+ */
+ private class DexSearchResult {
+ private String mOwningPackageName;
+ private int mOutcome;
+
+ public DexSearchResult(String owningPackageName, int outcome) {
+ this.mOwningPackageName = owningPackageName;
+ this.mOutcome = outcome;
+ }
+
+ @Override
+ public String toString() {
+ return mOwningPackageName + "-" + mOutcome;
+ }
+ }
+
+
+}
diff --git a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
new file mode 100644
index 0000000..10384a2
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright (C) 2016 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.dex;
+
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.os.Build;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.FastPrintWriter;
+import com.android.server.pm.AbstractStatsBase;
+import com.android.server.pm.PackageManagerServiceUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import dalvik.system.VMRuntime;
+import libcore.io.IoUtils;
+
+/**
+ * Stat file which store usage information about dex files.
+ */
+public class PackageDexUsage extends AbstractStatsBase<Void> {
+ private final static String TAG = "PackageDexUsage";
+
+ private final static int PACKAGE_DEX_USAGE_VERSION = 1;
+ private final static String PACKAGE_DEX_USAGE_VERSION_HEADER =
+ "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__";
+
+ private final static String SPLIT_CHAR = ",";
+ private final static String DEX_LINE_CHAR = "#";
+
+ // Map which structures the information we have on a package.
+ // Maps package name to package data (which stores info about UsedByOtherApps and
+ // secondary dex files.).
+ // Access to this map needs synchronized.
+ @GuardedBy("mPackageUseInfoMap")
+ private Map<String, PackageUseInfo> mPackageUseInfoMap;
+
+ public PackageDexUsage() {
+ super("package-dex-usage.list", "PackageDexUsage_DiskWriter", /*lock*/ false);
+ mPackageUseInfoMap = new HashMap<>();
+ }
+
+ /**
+ * Record a dex file load.
+ *
+ * Note this is called when apps load dex files and as such it should return
+ * as fast as possible.
+ *
+ * @param loadingPackage the package performing the load
+ * @param dexPath the path of the dex files being loaded
+ * @param ownerUserId the user id which runs the code loading the dex files
+ * @param loaderIsa the ISA of the app loading the dex files
+ * @param isUsedByOtherApps whether or not this dex file was not loaded by its owning package
+ * @param primaryOrSplit whether or not the dex file is a primary/split dex. True indicates
+ * the file is either primary or a split. False indicates the file is secondary dex.
+ * @return true if the dex load constitutes new information, or false if this information
+ * has been seen before.
+ */
+ public boolean record(String owningPackageName, String dexPath, int ownerUserId,
+ String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) {
+ if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
+ throw new IllegalArgumentException("loaderIsa " + loaderIsa + " is unsupported");
+ }
+ synchronized (mPackageUseInfoMap) {
+ PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(owningPackageName);
+ if (packageUseInfo == null) {
+ // This is the first time we see the package.
+ packageUseInfo = new PackageUseInfo();
+ if (primaryOrSplit) {
+ // If we have a primary or a split apk, set isUsedByOtherApps.
+ // We do not need to record the loaderIsa or the owner because we compile
+ // primaries for all users and all ISAs.
+ packageUseInfo.mIsUsedByOtherApps = isUsedByOtherApps;
+ } else {
+ // For secondary dex files record the loaderISA and the owner. We'll need
+ // to know under which user to compile and for what ISA.
+ packageUseInfo.mDexUseInfoMap.put(
+ dexPath, new DexUseInfo(isUsedByOtherApps, ownerUserId, loaderIsa));
+ }
+ mPackageUseInfoMap.put(owningPackageName, packageUseInfo);
+ return true;
+ } else {
+ // We already have data on this package. Amend it.
+ if (primaryOrSplit) {
+ // We have a possible update on the primary apk usage. Merge
+ // isUsedByOtherApps information and return if there was an update.
+ return packageUseInfo.merge(isUsedByOtherApps);
+ } else {
+ DexUseInfo newData = new DexUseInfo(
+ isUsedByOtherApps, ownerUserId, loaderIsa);
+ DexUseInfo existingData = packageUseInfo.mDexUseInfoMap.get(dexPath);
+ if (existingData == null) {
+ // It's the first time we see this dex file.
+ packageUseInfo.mDexUseInfoMap.put(dexPath, newData);
+ return true;
+ } else {
+ if (ownerUserId != existingData.mOwnerUserId) {
+ // Oups, this should never happen, the DexManager who calls this should
+ // do the proper checks and not call record if the user does not own the
+ // dex path.
+ // Secondary dex files are stored in the app user directory. A change in
+ // owningUser for the same path means that something went wrong at some
+ // higher level, and the loaderUser was allowed to cross
+ // user-boundaries and access data from what we know to be the owner
+ // user.
+ throw new IllegalArgumentException("Trying to change ownerUserId for "
+ + " dex path " + dexPath + " from " + existingData.mOwnerUserId
+ + " to " + ownerUserId);
+ }
+ // Merge the information into the existing data.
+ // Returns true if there was an update.
+ return existingData.merge(newData);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Convenience method for sync reads which does not force the user to pass a useless
+ * (Void) null.
+ */
+ public void read() {
+ read((Void) null);
+ }
+
+ /**
+ * Convenience method for async writes which does not force the user to pass a useless
+ * (Void) null.
+ */
+ public void maybeWriteAsync() {
+ maybeWriteAsync((Void) null);
+ }
+
+ @Override
+ protected void writeInternal(Void data) {
+ AtomicFile file = getFile();
+ FileOutputStream f = null;
+
+ try {
+ f = file.startWrite();
+ OutputStreamWriter osw = new OutputStreamWriter(f);
+ write(osw);
+ osw.flush();
+ file.finishWrite(f);
+ } catch (IOException e) {
+ if (f != null) {
+ file.failWrite(f);
+ }
+ Slog.e(TAG, "Failed to write usage for dex files", e);
+ }
+ }
+
+ /**
+ * File format:
+ *
+ * file_magic_version
+ * package_name_1
+ * #dex_file_path_1_1
+ * user_1_1, used_by_other_app_1_1, user_isa_1_1_1, user_isa_1_1_2
+ * #dex_file_path_1_2
+ * user_1_2, used_by_other_app_1_2, user_isa_1_2_1, user_isa_1_2_2
+ * ...
+ * package_name_2
+ * #dex_file_path_2_1
+ * user_2_1, used_by_other_app_2_1, user_isa_2_1_1, user_isa_2_1_2
+ * #dex_file_path_2_2,
+ * user_2_2, used_by_other_app_2_2, user_isa_2_2_1, user_isa_2_2_2
+ * ...
+ */
+ /* package */ void write(Writer out) {
+ // Make a clone to avoid locking while writing to disk.
+ Map<String, PackageUseInfo> packageUseInfoMapClone = clonePackageUseInfoMap();
+
+ FastPrintWriter fpw = new FastPrintWriter(out);
+
+ // Write the header.
+ fpw.print(PACKAGE_DEX_USAGE_VERSION_HEADER);
+ fpw.println(PACKAGE_DEX_USAGE_VERSION);
+
+ for (Map.Entry<String, PackageUseInfo> pEntry : packageUseInfoMapClone.entrySet()) {
+ // Write the package line.
+ String packageName = pEntry.getKey();
+ PackageUseInfo packageUseInfo = pEntry.getValue();
+
+ fpw.println(String.join(SPLIT_CHAR, packageName,
+ writeBoolean(packageUseInfo.mIsUsedByOtherApps)));
+
+ // Write dex file lines.
+ for (Map.Entry<String, DexUseInfo> dEntry : packageUseInfo.mDexUseInfoMap.entrySet()) {
+ String dexPath = dEntry.getKey();
+ DexUseInfo dexUseInfo = dEntry.getValue();
+ fpw.println(DEX_LINE_CHAR + dexPath);
+ fpw.print(String.join(SPLIT_CHAR, Integer.toString(dexUseInfo.mOwnerUserId),
+ writeBoolean(dexUseInfo.mIsUsedByOtherApps)));
+ for (String isa : dexUseInfo.mLoaderIsas) {
+ fpw.print(SPLIT_CHAR + isa);
+ }
+ fpw.println();
+ }
+ }
+ fpw.flush();
+ }
+
+ @Override
+ protected void readInternal(Void data) {
+ AtomicFile file = getFile();
+ BufferedReader in = null;
+ try {
+ in = new BufferedReader(new InputStreamReader(file.openRead()));
+ read(in);
+ } catch (FileNotFoundException expected) {
+ // The file may not be there. E.g. When we first take the OTA with this feature.
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to parse package dex usage.", e);
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+
+ /* package */ void read(Reader reader) throws IOException {
+ Map<String, PackageUseInfo> data = new HashMap<>();
+ BufferedReader in = new BufferedReader(reader);
+ // Read header, do version check.
+ String versionLine = in.readLine();
+ if (versionLine == null) {
+ throw new IllegalStateException("No version line found.");
+ } else {
+ if (!versionLine.startsWith(PACKAGE_DEX_USAGE_VERSION_HEADER)) {
+ // TODO(calin): the caller is responsible to clear the file.
+ throw new IllegalStateException("Invalid version line: " + versionLine);
+ }
+ int version = Integer.parseInt(
+ versionLine.substring(PACKAGE_DEX_USAGE_VERSION_HEADER.length()));
+ if (version != PACKAGE_DEX_USAGE_VERSION) {
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+ }
+
+ String s = null;
+ String currentPakage = null;
+ PackageUseInfo currentPakageData = null;
+
+ Set<String> supportedIsas = new HashSet<>();
+ for (String abi : Build.SUPPORTED_ABIS) {
+ supportedIsas.add(VMRuntime.getInstructionSet(abi));
+ }
+ while ((s = in.readLine()) != null) {
+ if (s.startsWith(DEX_LINE_CHAR)) {
+ // This is the start of the the dex lines.
+ // We expect two lines for each dex entry:
+ // #dexPaths
+ // onwerUserId,isUsedByOtherApps,isa1,isa2
+ if (currentPakage == null) {
+ throw new IllegalStateException(
+ "Malformed PackageDexUsage file. Expected package line before dex line.");
+ }
+
+ // First line is the dex path.
+ String dexPath = s.substring(DEX_LINE_CHAR.length());
+ // Next line is the dex data.
+ s = in.readLine();
+ if (s == null) {
+ throw new IllegalStateException("Could not fine dexUseInfo for line: " + s);
+ }
+
+ // We expect at least 3 elements (isUsedByOtherApps, userId, isa).
+ String[] elems = s.split(SPLIT_CHAR);
+ if (elems.length < 3) {
+ throw new IllegalStateException("Invalid PackageDexUsage line: " + s);
+ }
+ int ownerUserId = Integer.parseInt(elems[0]);
+ boolean isUsedByOtherApps = readBoolean(elems[1]);
+ DexUseInfo dexUseInfo = new DexUseInfo(isUsedByOtherApps, ownerUserId);
+ for (int i = 2; i < elems.length; i++) {
+ String isa = elems[i];
+ if (supportedIsas.contains(isa)) {
+ dexUseInfo.mLoaderIsas.add(elems[i]);
+ } else {
+ // Should never happen unless someone crafts the file manually.
+ // In theory it could if we drop a supported ISA after an OTA but we don't
+ // do that.
+ Slog.wtf(TAG, "Unsupported ISA when parsing PackageDexUsage: " + isa);
+ }
+ }
+ if (supportedIsas.isEmpty()) {
+ Slog.wtf(TAG, "Ignore dexPath when parsing PackageDexUsage because of " +
+ "unsupported isas. dexPath=" + dexPath);
+ continue;
+ }
+ currentPakageData.mDexUseInfoMap.put(dexPath, dexUseInfo);
+ } else {
+ // This is a package line.
+ // We expect it to be: `packageName,isUsedByOtherApps`.
+ String[] elems = s.split(SPLIT_CHAR);
+ if (elems.length != 2) {
+ throw new IllegalStateException("Invalid PackageDexUsage line: " + s);
+ }
+ currentPakage = elems[0];
+ currentPakageData = new PackageUseInfo();
+ currentPakageData.mIsUsedByOtherApps = readBoolean(elems[1]);
+ data.put(currentPakage, currentPakageData);
+ }
+ }
+
+ synchronized (mPackageUseInfoMap) {
+ mPackageUseInfoMap.clear();
+ mPackageUseInfoMap.putAll(data);
+ }
+ }
+
+ /**
+ * Syncs the existing data with the set of available packages by removing obsolete entries.
+ */
+ public void syncData(Map<String, Set<Integer>> packageToUsersMap) {
+ synchronized (mPackageUseInfoMap) {
+ Iterator<Map.Entry<String, PackageUseInfo>> pIt =
+ mPackageUseInfoMap.entrySet().iterator();
+ while (pIt.hasNext()) {
+ Map.Entry<String, PackageUseInfo> pEntry = pIt.next();
+ String packageName = pEntry.getKey();
+ PackageUseInfo packageUseInfo = pEntry.getValue();
+ Set<Integer> users = packageToUsersMap.get(packageName);
+ if (users == null) {
+ // The package doesn't exist anymore, remove the record.
+ pIt.remove();
+ } else {
+ // The package exists but we can prune the entries associated with non existing
+ // users.
+ Iterator<Map.Entry<String, DexUseInfo>> dIt =
+ packageUseInfo.mDexUseInfoMap.entrySet().iterator();
+ while (dIt.hasNext()) {
+ DexUseInfo dexUseInfo = dIt.next().getValue();
+ if (!users.contains(dexUseInfo.mOwnerUserId)) {
+ // User was probably removed. Delete its dex usage info.
+ dIt.remove();
+ }
+ }
+ if (!packageUseInfo.mIsUsedByOtherApps
+ && packageUseInfo.mDexUseInfoMap.isEmpty()) {
+ // The package is not used by other apps and we removed all its dex files
+ // records. Remove the entire package record as well.
+ pIt.remove();
+ }
+ }
+ }
+ }
+ }
+
+ public PackageUseInfo getPackageUseInfo(String packageName) {
+ synchronized (mPackageUseInfoMap) {
+ return mPackageUseInfoMap.get(packageName);
+ }
+ }
+
+ public void clear() {
+ synchronized (mPackageUseInfoMap) {
+ mPackageUseInfoMap.clear();
+ }
+ }
+ // Creates a deep copy of the class' mPackageUseInfoMap.
+ private Map<String, PackageUseInfo> clonePackageUseInfoMap() {
+ Map<String, PackageUseInfo> clone = new HashMap<>();
+ synchronized (mPackageUseInfoMap) {
+ for (Map.Entry<String, PackageUseInfo> e : mPackageUseInfoMap.entrySet()) {
+ clone.put(e.getKey(), new PackageUseInfo(e.getValue()));
+ }
+ }
+ return clone;
+ }
+
+ private String writeBoolean(boolean bool) {
+ return bool ? "1" : "0";
+ }
+
+ private boolean readBoolean(String bool) {
+ if ("0".equals(bool)) return false;
+ if ("1".equals(bool)) return true;
+ throw new IllegalArgumentException("Unknown bool encoding: " + bool);
+ }
+
+ private boolean contains(int[] array, int elem) {
+ for (int i = 0; i < array.length; i++) {
+ if (elem == array[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String dump() {
+ StringWriter sw = new StringWriter();
+ write(sw);
+ return sw.toString();
+ }
+
+ /**
+ * Stores data on how a package and its dex files are used.
+ */
+ public static class PackageUseInfo {
+ // This flag is for the primary and split apks. It is set to true whenever one of them
+ // is loaded by another app.
+ private boolean mIsUsedByOtherApps;
+ // Map dex paths to their data (isUsedByOtherApps, owner id, loader isa).
+ private final Map<String, DexUseInfo> mDexUseInfoMap;
+
+ public PackageUseInfo() {
+ mIsUsedByOtherApps = false;
+ mDexUseInfoMap = new HashMap<>();
+ }
+
+ // Creates a deep copy of the `other`.
+ public PackageUseInfo(PackageUseInfo other) {
+ mIsUsedByOtherApps = other.mIsUsedByOtherApps;
+ mDexUseInfoMap = new HashMap<>();
+ for (Map.Entry<String, DexUseInfo> e : other.mDexUseInfoMap.entrySet()) {
+ mDexUseInfoMap.put(e.getKey(), new DexUseInfo(e.getValue()));
+ }
+ }
+
+ private boolean merge(boolean isUsedByOtherApps) {
+ boolean oldIsUsedByOtherApps = mIsUsedByOtherApps;
+ mIsUsedByOtherApps = mIsUsedByOtherApps || isUsedByOtherApps;
+ return oldIsUsedByOtherApps != this.mIsUsedByOtherApps;
+ }
+
+ public boolean isUsedByOtherApps() {
+ return mIsUsedByOtherApps;
+ }
+
+ public Map<String, DexUseInfo> getDexUseInfoMap() {
+ return mDexUseInfoMap;
+ }
+ }
+
+ /**
+ * Stores data about a loaded dex files.
+ */
+ public static class DexUseInfo {
+ private boolean mIsUsedByOtherApps;
+ private final int mOwnerUserId;
+ private final Set<String> mLoaderIsas;
+
+ public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId) {
+ this(isUsedByOtherApps, ownerUserId, null);
+ }
+
+ public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId, String loaderIsa) {
+ mIsUsedByOtherApps = isUsedByOtherApps;
+ mOwnerUserId = ownerUserId;
+ mLoaderIsas = new HashSet<>();
+ if (loaderIsa != null) {
+ mLoaderIsas.add(loaderIsa);
+ }
+ }
+
+ // Creates a deep copy of the `other`.
+ public DexUseInfo(DexUseInfo other) {
+ mIsUsedByOtherApps = other.mIsUsedByOtherApps;
+ mOwnerUserId = other.mOwnerUserId;
+ mLoaderIsas = new HashSet<>(other.mLoaderIsas);
+ }
+
+ private boolean merge(DexUseInfo dexUseInfo) {
+ boolean oldIsUsedByOtherApps = mIsUsedByOtherApps;
+ mIsUsedByOtherApps = mIsUsedByOtherApps || dexUseInfo.mIsUsedByOtherApps;
+ boolean updateIsas = mLoaderIsas.addAll(dexUseInfo.mLoaderIsas);
+ return updateIsas || (oldIsUsedByOtherApps != mIsUsedByOtherApps);
+ }
+
+ public boolean isUsedByOtherApps() {
+ return mIsUsedByOtherApps;
+ }
+
+ public int getOwnerUserId() {
+ return mOwnerUserId;
+ }
+
+ public Set<String> getLoaderIsas() {
+ return mLoaderIsas;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
index c653b8e..69d27f2 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
@@ -122,6 +122,8 @@
when(mContext.getResources()).thenReturn(mResources);
mNetworkScoreService = new NetworkScoreService(mContext, mNetworkScorerAppManager);
WifiConfiguration configuration = new WifiConfiguration();
+ configuration.SSID = "NetworkScoreServiceTest_SSID";
+ configuration.BSSID = "NetworkScoreServiceTest_BSSID";
mRecommendationRequest = new RecommendationRequest.Builder()
.setCurrentRecommendedWifiConfig(configuration).build();
}
@@ -232,9 +234,10 @@
injectProvider();
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
final WifiConfiguration wifiConfiguration = new WifiConfiguration();
- wifiConfiguration.SSID = "testRequestRecommendation_resultReturned";
- final RecommendationResult providerResult =
- new RecommendationResult(wifiConfiguration);
+ wifiConfiguration.SSID = "testRequestRecommendation_resultReturned_SSID";
+ wifiConfiguration.BSSID = "testRequestRecommendation_resultReturned_BSSID";
+ final RecommendationResult providerResult = RecommendationResult
+ .createConnectRecommendation(wifiConfiguration);
final Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_RECOMMENDATION_RESULT, providerResult);
doAnswer(invocation -> {
@@ -250,6 +253,8 @@
assertNotNull(result);
assertEquals(providerResult.getWifiConfiguration().SSID,
result.getWifiConfiguration().SSID);
+ assertEquals(providerResult.getWifiConfiguration().BSSID,
+ result.getWifiConfiguration().BSSID);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
new file mode 100644
index 0000000..b655f3a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2016 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.dex;
+
+import android.os.Build;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import dalvik.system.VMRuntime;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DexManagerTests {
+ private DexManager mDexManager;
+
+ private TestData mFooUser0;
+ private TestData mBarUser0;
+ private TestData mBarUser1;
+ private TestData mInvalidIsa;
+ private TestData mDoesNotExist;
+
+ private int mUser0;
+ private int mUser1;
+ @Before
+ public void setup() {
+
+ mUser0 = 0;
+ mUser1 = 1;
+
+ String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
+ String foo = "foo";
+ String bar = "bar";
+
+ mFooUser0 = new TestData(foo, isa, mUser0);
+ mBarUser0 = new TestData(bar, isa, mUser0);
+ mBarUser1 = new TestData(bar, isa, mUser1);
+ mInvalidIsa = new TestData("INVALID", "INVALID_ISA", mUser0);
+ mDoesNotExist = new TestData("DOES.NOT.EXIST", isa, mUser1);
+
+
+ mDexManager = new DexManager();
+
+ // Foo and Bar are available to user0.
+ // Only Bar is available to user1;
+ Map<Integer, List<PackageInfo>> existingPackages = new HashMap<>();
+ existingPackages.put(mUser0, Arrays.asList(mFooUser0.mPackageInfo, mBarUser0.mPackageInfo));
+ existingPackages.put(mUser1, Arrays.asList(mBarUser1.mPackageInfo));
+ mDexManager.load(existingPackages);
+ }
+
+ @Test
+ public void testNotifyPrimaryUse() {
+ // The main dex file and splits are re-loaded by the app.
+ notifyDexLoad(mFooUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser0);
+
+ // Package is not used by others, so we should get nothing back.
+ assertNull(getPackageUseInfo(mFooUser0));
+ }
+
+ @Test
+ public void testNotifyPrimaryForeignUse() {
+ // Foo loads Bar main apks.
+ notifyDexLoad(mFooUser0, mBarUser0.getBaseAndSplitDexPaths(), mUser0);
+
+ // Bar is used by others now and should be in our records
+ PackageUseInfo pui = getPackageUseInfo(mBarUser0);
+ assertNotNull(pui);
+ assertTrue(pui.isUsedByOtherApps());
+ assertTrue(pui.getDexUseInfoMap().isEmpty());
+ }
+
+ @Test
+ public void testNotifySecondary() {
+ // Foo loads its own secondary files.
+ List<String> fooSecondaries = mFooUser0.getSecondaryDexPaths();
+ notifyDexLoad(mFooUser0, fooSecondaries, mUser0);
+
+ PackageUseInfo pui = getPackageUseInfo(mFooUser0);
+ assertNotNull(pui);
+ assertFalse(pui.isUsedByOtherApps());
+ assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
+ assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+ }
+
+ @Test
+ public void testNotifySecondaryForeign() {
+ // Foo loads bar secondary files.
+ List<String> barSecondaries = mBarUser0.getSecondaryDexPaths();
+ notifyDexLoad(mFooUser0, barSecondaries, mUser0);
+
+ PackageUseInfo pui = getPackageUseInfo(mBarUser0);
+ assertNotNull(pui);
+ assertFalse(pui.isUsedByOtherApps());
+ assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size());
+ assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
+ }
+
+ @Test
+ public void testNotifySequence() {
+ // Foo loads its own secondary files.
+ List<String> fooSecondaries = mFooUser0.getSecondaryDexPaths();
+ notifyDexLoad(mFooUser0, fooSecondaries, mUser0);
+ // Foo loads Bar own secondary files.
+ List<String> barSecondaries = mBarUser0.getSecondaryDexPaths();
+ notifyDexLoad(mFooUser0, barSecondaries, mUser0);
+ // Foo loads Bar primary files.
+ notifyDexLoad(mFooUser0, mBarUser0.getBaseAndSplitDexPaths(), mUser0);
+ // Bar loads its own secondary files.
+ notifyDexLoad(mBarUser0, barSecondaries, mUser0);
+ // Bar loads some own secondary files which foo didn't load.
+ List<String> barSecondariesForOwnUse = mBarUser0.getSecondaryDexPathsForOwnUse();
+ notifyDexLoad(mBarUser0, barSecondariesForOwnUse, mUser0);
+
+ // Check bar usage. Should be used by other app (for primary and barSecondaries).
+ PackageUseInfo pui = getPackageUseInfo(mBarUser0);
+ assertNotNull(pui);
+ assertTrue(pui.isUsedByOtherApps());
+ assertEquals(barSecondaries.size() + barSecondariesForOwnUse.size(),
+ pui.getDexUseInfoMap().size());
+
+ assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
+ assertSecondaryUse(mFooUser0, pui, barSecondariesForOwnUse,
+ /*isUsedByOtherApps*/false, mUser0);
+
+ // Check foo usage. Should not be used by other app.
+ pui = getPackageUseInfo(mFooUser0);
+ assertNotNull(pui);
+ assertFalse(pui.isUsedByOtherApps());
+ assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
+ assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+ }
+
+ @Test
+ public void testPackageUseInfoNotFound() {
+ // Assert we don't get back data we did not previously record.
+ assertNull(getPackageUseInfo(mFooUser0));
+ }
+
+ @Test
+ public void testInvalidIsa() {
+ // Notifying with an invalid ISA should be ignored.
+ notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0);
+ assertNull(getPackageUseInfo(mInvalidIsa));
+ }
+
+ @Test
+ public void testNotExistingPackate() {
+ // Notifying about the load of a package which was previously not
+ // register in DexManager#load should be ignored.
+ notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0);
+ assertNull(getPackageUseInfo(mDoesNotExist));
+ }
+
+ @Test
+ public void testCrossUserAttempt() {
+ // Bar from User1 tries to load secondary dex files from User0 Bar.
+ // Request should be ignored.
+ notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1);
+ assertNull(getPackageUseInfo(mBarUser1));
+ }
+
+ @Test
+ public void testPackageNotInstalledForUser() {
+ // User1 tries to load Foo which is installed for User0 but not for User1.
+ // Note that the PackageManagerService already filters this out but we
+ // still check that nothing goes unexpected in DexManager.
+ notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1);
+ assertNull(getPackageUseInfo(mBarUser1));
+ }
+
+ private void assertSecondaryUse(TestData testData, PackageUseInfo pui,
+ List<String> secondaries, boolean isUsedByOtherApps, int ownerUserId) {
+ for (String dex : secondaries) {
+ DexUseInfo dui = pui.getDexUseInfoMap().get(dex);
+ assertNotNull(dui);
+ assertEquals(isUsedByOtherApps, dui.isUsedByOtherApps());
+ assertEquals(ownerUserId, dui.getOwnerUserId());
+ assertEquals(1, dui.getLoaderIsas().size());
+ assertTrue(dui.getLoaderIsas().contains(testData.mLoaderIsa));
+ }
+ }
+
+ private void notifyDexLoad(TestData testData, List<String> dexPaths, int loaderUserId) {
+ mDexManager.notifyDexLoad(testData.mPackageInfo.applicationInfo, dexPaths,
+ testData.mLoaderIsa, loaderUserId);
+ }
+
+ private PackageUseInfo getPackageUseInfo(TestData testData) {
+ return mDexManager.getPackageUseInfo(testData.mPackageInfo.packageName);
+ }
+
+ private static PackageInfo getMockPackageInfo(String packageName, int userId) {
+ PackageInfo pi = new PackageInfo();
+ pi.packageName = packageName;
+ pi.applicationInfo = getMockApplicationInfo(packageName, userId);
+ return pi;
+ }
+
+ private static ApplicationInfo getMockApplicationInfo(String packageName, int userId) {
+ ApplicationInfo ai = new ApplicationInfo();
+ String codeDir = "/data/app/" + packageName;
+ ai.setBaseCodePath(codeDir + "/base.dex");
+ ai.setSplitCodePaths(new String[] {codeDir + "/split-1.dex", codeDir + "/split-2.dex"});
+ ai.dataDir = "/data/user/" + userId + "/" + packageName;
+ ai.packageName = packageName;
+ return ai;
+ }
+
+ private static class TestData {
+ private final PackageInfo mPackageInfo;
+ private final String mLoaderIsa;
+
+ private TestData(String packageName, String loaderIsa, int userId) {
+ mPackageInfo = getMockPackageInfo(packageName, userId);
+ mLoaderIsa = loaderIsa;
+ }
+
+ private String getPackageName() {
+ return mPackageInfo.packageName;
+ }
+
+ List<String> getSecondaryDexPaths() {
+ List<String> paths = new ArrayList<>();
+ paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary1.dex");
+ paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary2.dex");
+ paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary3.dex");
+ return paths;
+ }
+
+ List<String> getSecondaryDexPathsForOwnUse() {
+ List<String> paths = new ArrayList<>();
+ paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary4.dex");
+ paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary5.dex");
+ return paths;
+ }
+
+ List<String> getBaseAndSplitDexPaths() {
+ List<String> paths = new ArrayList<>();
+ paths.add(mPackageInfo.applicationInfo.sourceDir);
+ for (String split : mPackageInfo.applicationInfo.splitSourceDirs) {
+ paths.add(split);
+ }
+ return paths;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
new file mode 100644
index 0000000..5a42841
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2016 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.dex;
+
+import android.os.Build;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import dalvik.system.VMRuntime;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PackageDexUsageTests {
+ private PackageDexUsage mPackageDexUsage;
+
+ private TestData mFooBaseUser0;
+ private TestData mFooSplit1User0;
+ private TestData mFooSplit2UsedByOtherApps0;
+ private TestData mFooSecondary1User0;
+ private TestData mFooSecondary1User1;
+ private TestData mFooSecondary2UsedByOtherApps0;
+ private TestData mInvalidIsa;
+
+ private TestData mBarBaseUser0;
+ private TestData mBarSecondary1User0;
+ private TestData mBarSecondary2User1;
+
+ @Before
+ public void setup() {
+ mPackageDexUsage = new PackageDexUsage();
+
+ String fooPackageName = "com.google.foo";
+ String fooCodeDir = "/data/app/com.google.foo/";
+ String fooDataDir = "/data/user/0/com.google.foo/";
+
+ String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
+
+ mFooBaseUser0 = new TestData(fooPackageName,
+ fooCodeDir + "base.apk", 0, isa, false, true);
+
+ mFooSplit1User0 = new TestData(fooPackageName,
+ fooCodeDir + "split-1.apk", 0, isa, false, true);
+
+ mFooSplit2UsedByOtherApps0 = new TestData(fooPackageName,
+ fooCodeDir + "split-2.apk", 0, isa, true, true);
+
+ mFooSecondary1User0 = new TestData(fooPackageName,
+ fooDataDir + "sec-1.dex", 0, isa, false, false);
+
+ mFooSecondary1User1 = new TestData(fooPackageName,
+ fooDataDir + "sec-1.dex", 1, isa, false, false);
+
+ mFooSecondary2UsedByOtherApps0 = new TestData(fooPackageName,
+ fooDataDir + "sec-2.dex", 0, isa, true, false);
+
+ mInvalidIsa = new TestData(fooPackageName,
+ fooCodeDir + "base.apk", 0, "INVALID_ISA", false, true);
+
+ String barPackageName = "com.google.bar";
+ String barCodeDir = "/data/app/com.google.bar/";
+ String barDataDir = "/data/user/0/com.google.bar/";
+ String barDataDir1 = "/data/user/1/com.google.bar/";
+
+ mBarBaseUser0 = new TestData(barPackageName,
+ barCodeDir + "base.apk", 0, isa, false, true);
+ mBarSecondary1User0 = new TestData(barPackageName,
+ barDataDir + "sec-1.dex", 0, isa, false, false);
+ mBarSecondary2User1 = new TestData(barPackageName,
+ barDataDir1 + "sec-2.dex", 1, isa, false, false);
+ }
+
+ @Test
+ public void testRecordPrimary() {
+ // Assert new information.
+ assertTrue(record(mFooBaseUser0));
+
+ assertPackageDexUsage(mFooBaseUser0);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooBaseUser0);
+ }
+
+ @Test
+ public void testRecordSplit() {
+ // Assert new information.
+ assertTrue(record(mFooSplit1User0));
+
+ assertPackageDexUsage(mFooSplit1User0);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooSplit1User0);
+ }
+
+ @Test
+ public void testRecordSplitPrimarySequence() {
+ // Assert new information.
+ assertTrue(record(mFooBaseUser0));
+ // Assert no new information.
+ assertFalse(record(mFooSplit1User0));
+
+ assertPackageDexUsage(mFooBaseUser0);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooBaseUser0);
+
+ // Write Split2 which is used by other apps.
+ // Assert new information.
+ assertTrue(record(mFooSplit2UsedByOtherApps0));
+ assertPackageDexUsage(mFooSplit2UsedByOtherApps0);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooSplit2UsedByOtherApps0);
+ }
+
+ @Test
+ public void testRecordSecondary() {
+ assertTrue(record(mFooSecondary1User0));
+
+ assertPackageDexUsage(null, mFooSecondary1User0);
+ writeAndReadBack();
+ assertPackageDexUsage(null, mFooSecondary1User0);
+
+ // Recording again does not add more data.
+ assertFalse(record(mFooSecondary1User0));
+ assertPackageDexUsage(null, mFooSecondary1User0);
+ }
+
+ @Test
+ public void testRecordBaseAndSecondarySequence() {
+ // Write split.
+ assertTrue(record(mFooSplit2UsedByOtherApps0));
+ // Write secondary.
+ assertTrue(record(mFooSecondary1User0));
+
+ // Check.
+ assertPackageDexUsage(mFooSplit2UsedByOtherApps0, mFooSecondary1User0);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooSplit2UsedByOtherApps0, mFooSecondary1User0);
+
+ // Write another secondary.
+ assertTrue(record(mFooSecondary2UsedByOtherApps0));
+
+ // Check.
+ assertPackageDexUsage(
+ mFooSplit2UsedByOtherApps0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
+ writeAndReadBack();
+ assertPackageDexUsage(
+ mFooSplit2UsedByOtherApps0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
+ }
+
+ @Test
+ public void testMultiplePackages() {
+ assertTrue(record(mFooBaseUser0));
+ assertTrue(record(mFooSecondary1User0));
+ assertTrue(record(mFooSecondary2UsedByOtherApps0));
+ assertTrue(record(mBarBaseUser0));
+ assertTrue(record(mBarSecondary1User0));
+ assertTrue(record(mBarSecondary2User1));
+
+ assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
+ assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
+ assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
+ }
+
+ @Test
+ public void testPackageNotFound() {
+ assertNull(mPackageDexUsage.getPackageUseInfo("missing.package"));
+ }
+
+ @Test
+ public void testAttemptToChangeOwner() {
+ assertTrue(record(mFooSecondary1User0));
+ try {
+ record(mFooSecondary1User1);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testInvalidIsa() {
+ try {
+ record(mInvalidIsa);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testReadWriteEmtpy() {
+ // Expect no exceptions when writing/reading without data.
+ writeAndReadBack();
+ }
+
+ @Test
+ public void testSyncData() {
+ // Write some records.
+ assertTrue(record(mFooBaseUser0));
+ assertTrue(record(mFooSecondary1User0));
+ assertTrue(record(mFooSecondary2UsedByOtherApps0));
+ assertTrue(record(mBarBaseUser0));
+ assertTrue(record(mBarSecondary1User0));
+ assertTrue(record(mBarSecondary2User1));
+
+ // Verify all is good.
+ assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
+ assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
+ writeAndReadBack();
+ assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0);
+ assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1);
+
+ // Simulate that only user 1 is available.
+ Map<String, Set<Integer>> packageToUsersMap = new HashMap<>();
+ packageToUsersMap.put(mBarSecondary2User1.mPackageName,
+ new HashSet<>(Arrays.asList(mBarSecondary2User1.mOwnerUserId)));
+ mPackageDexUsage.syncData(packageToUsersMap);
+
+ // Assert that only user 1 files are there.
+ assertPackageDexUsage(mBarBaseUser0, mBarSecondary2User1);
+ assertNull(mPackageDexUsage.getPackageUseInfo(mFooBaseUser0.mPackageName));
+ }
+
+ private void assertPackageDexUsage(TestData primary, TestData... secondaries) {
+ String packageName = primary == null ? secondaries[0].mPackageName : primary.mPackageName;
+ boolean primaryUsedByOtherApps = primary == null ? false : primary.mUsedByOtherApps;
+ PackageUseInfo pInfo = mPackageDexUsage.getPackageUseInfo(packageName);
+
+ // Check package use info
+ assertNotNull(pInfo);
+ assertEquals(primaryUsedByOtherApps, pInfo.isUsedByOtherApps());
+ Map<String, DexUseInfo> dexUseInfoMap = pInfo.getDexUseInfoMap();
+ assertEquals(secondaries.length, dexUseInfoMap.size());
+
+ // Check dex use info
+ for (TestData testData : secondaries) {
+ DexUseInfo dInfo = dexUseInfoMap.get(testData.mDexFile);
+ assertNotNull(dInfo);
+ assertEquals(testData.mUsedByOtherApps, dInfo.isUsedByOtherApps());
+ assertEquals(testData.mOwnerUserId, dInfo.getOwnerUserId());
+ assertEquals(1, dInfo.getLoaderIsas().size());
+ assertTrue(dInfo.getLoaderIsas().contains(testData.mLoaderIsa));
+ }
+ }
+
+ private boolean record(TestData testData) {
+ return mPackageDexUsage.record(testData.mPackageName, testData.mDexFile,
+ testData.mOwnerUserId, testData.mLoaderIsa, testData.mUsedByOtherApps,
+ testData.mPrimaryOrSplit);
+ }
+
+ private void writeAndReadBack() {
+ try {
+ StringWriter writer = new StringWriter();
+ mPackageDexUsage.write(writer);
+
+ mPackageDexUsage = new PackageDexUsage();
+ mPackageDexUsage.read(new StringReader(writer.toString()));
+ } catch (IOException e) {
+ fail("Unexpected IOException: " + e.getMessage());
+ }
+ }
+
+ private static class TestData {
+ private final String mPackageName;
+ private final String mDexFile;
+ private final int mOwnerUserId;
+ private final String mLoaderIsa;
+ private final boolean mUsedByOtherApps;
+ private final boolean mPrimaryOrSplit;
+
+ private TestData(String packageName, String dexFile, int ownerUserId,
+ String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) {
+ mPackageName = packageName;
+ mDexFile = dexFile;
+ mOwnerUserId = ownerUserId;
+ mLoaderIsa = loaderIsa;
+ mUsedByOtherApps = isUsedByOtherApps;
+ mPrimaryOrSplit = primaryOrSplit;
+ }
+
+ }
+}
diff --git a/tools/incident_report/Android.mk b/tools/incident_report/Android.mk
index ed89bd6..9e56e3d 100644
--- a/tools/incident_report/Android.mk
+++ b/tools/incident_report/Android.mk
@@ -34,6 +34,8 @@
libplatformprotos \
libprotobuf-cpp-full
+LOCAL_C_FLAGS := \
+ -Wno-unused-parameter
include $(BUILD_HOST_EXECUTABLE)
diff --git a/tools/incident_report/main.cpp b/tools/incident_report/main.cpp
index a814847..884b8e4 100644
--- a/tools/incident_report/main.cpp
+++ b/tools/incident_report/main.cpp
@@ -17,7 +17,7 @@
#include "generic_message.h"
#include "printer.h"
-#include <frameworks/base/core/proto/android/os/incident_proto.pb.h>
+#include <frameworks/base/core/proto/android/os/incident.pb.h>
#include <google/protobuf/wire_format.h>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>
diff --git a/tools/incident_report/printer.cpp b/tools/incident_report/printer.cpp
index 8111b27..bd660dd2 100644
--- a/tools/incident_report/printer.cpp
+++ b/tools/incident_report/printer.cpp
@@ -69,8 +69,10 @@
va_start(args, format);
len = vsnprintf(mBuf, mBufSize, format, args);
+ va_end(args);
bool truncated = (len >= mBufSize) && (reallocate(len) < len);
-
+
+ va_start(args, format);
len = vsnprintf(mBuf, mBufSize, format, args);
va_end(args);
diff --git a/tools/incident_section_gen/main.cpp b/tools/incident_section_gen/main.cpp
index d004810..15f622c 100644
--- a/tools/incident_section_gen/main.cpp
+++ b/tools/incident_section_gen/main.cpp
@@ -15,7 +15,7 @@
*/
-#include <frameworks/base/core/proto/android/os/incident_proto.pb.h>
+#include <frameworks/base/core/proto/android/os/incident.pb.h>
#include <map>
diff --git a/wifi/java/android/net/wifi/ScanResult.java b/wifi/java/android/net/wifi/ScanResult.java
index da87135..da9aa06 100644
--- a/wifi/java/android/net/wifi/ScanResult.java
+++ b/wifi/java/android/net/wifi/ScanResult.java
@@ -16,6 +16,7 @@
package android.net.wifi;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -259,10 +260,10 @@
public long blackListTimestamp;
/**
- * Status: indicating the scan result is not a result
- * that is part of user's saved configurations
+ * Status indicating the scan result does not correspond to a user's saved configuration
* @hide
*/
+ @SystemApi
public boolean untrusted;
/**
diff --git a/wifi/java/android/net/wifi/WifiNetworkScoreCache.java b/wifi/java/android/net/wifi/WifiNetworkScoreCache.java
index c328748..9dd118b 100755
--- a/wifi/java/android/net/wifi/WifiNetworkScoreCache.java
+++ b/wifi/java/android/net/wifi/WifiNetworkScoreCache.java
@@ -17,13 +17,19 @@
package android.net.wifi;
import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Context;
+import android.os.Handler;
import android.net.INetworkScoreCache;
import android.net.NetworkKey;
import android.net.ScoredNetwork;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashMap;
@@ -43,30 +49,55 @@
// We treat the lowest possible score as though there were no score, effectively allowing the
// scorer to provide an RSSI threshold below which a network should not be used.
public static final int INVALID_NETWORK_SCORE = Byte.MIN_VALUE;
+
+ // See {@link #CacheListener}.
+ @Nullable
+ @GuardedBy("mCacheLock")
+ private CacheListener mListener;
+
private final Context mContext;
+ private final Object mCacheLock = new Object();
// The key is of the form "<ssid>"<bssid>
// TODO: What about SSIDs that can't be encoded as UTF-8?
private final Map<String, ScoredNetwork> mNetworkCache;
+
public WifiNetworkScoreCache(Context context) {
- mContext = context;
+ this(context, null /* listener */);
+ }
+
+ /**
+ * Instantiates a WifiNetworkScoreCache.
+ *
+ * @param context Application context
+ * @param listener CacheListener for cache updates
+ */
+ public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
+ mContext = context.getApplicationContext();
+ mListener = listener;
mNetworkCache = new HashMap<String, ScoredNetwork>();
}
@Override public final void updateScores(List<ScoredNetwork> networks) {
- if (networks == null) {
+ if (networks == null || networks.isEmpty()) {
return;
- }
- Log.e(TAG, "updateScores list size=" + networks.size());
+ }
+ Log.d(TAG, "updateScores list size=" + networks.size());
- synchronized(mNetworkCache) {
- for (ScoredNetwork network : networks) {
- String networkKey = buildNetworkKey(network);
- if (networkKey == null) continue;
- mNetworkCache.put(networkKey, network);
- }
- }
+ synchronized(mNetworkCache) {
+ for (ScoredNetwork network : networks) {
+ String networkKey = buildNetworkKey(network);
+ if (networkKey == null) continue;
+ mNetworkCache.put(networkKey, network);
+ }
+ }
+
+ synchronized (mCacheLock) {
+ if (mListener != null) {
+ mListener.post(networks);
+ }
+ }
}
@Override public final void clearScores() {
@@ -193,4 +224,53 @@
}
}
+ /** Registers a CacheListener instance, replacing the previous listener if it existed. */
+ public void registerListener(CacheListener listener) {
+ synchronized (mCacheLock) {
+ mListener = listener;
+ }
+ }
+
+ /** Removes the registered CacheListener. */
+ public void unregisterListener() {
+ synchronized (mCacheLock) {
+ mListener = null;
+ }
+ }
+
+ /** Listener for updates to the cache inside WifiNetworkScoreCache. */
+ public abstract static class CacheListener {
+
+ private Handler mHandler;
+
+ /**
+ * Constructor for CacheListener.
+ *
+ * @param handler the Handler on which to invoke the {@link #networkCacheUpdated} method.
+ * This cannot be null.
+ */
+ public CacheListener(@NonNull Handler handler) {
+ Preconditions.checkNotNull(handler);
+ mHandler = handler;
+ }
+
+ /** Invokes the {@link #networkCacheUpdated(List<ScoredNetwork>)} method on the handler. */
+ void post(List<ScoredNetwork> updatedNetworks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ networkCacheUpdated(updatedNetworks);
+ }
+ });
+ }
+
+ /**
+ * Invoked whenever the cache is updated.
+ *
+ * <p>Clearing the cache does not invoke this method.
+ *
+ * @param updatedNetworks the networks that were updated
+ */
+ public abstract void networkCacheUpdated(List<ScoredNetwork> updatedNetworks);
+ }
}
diff --git a/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java b/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java
index f8549b9..12d4995 100644
--- a/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java
+++ b/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java
@@ -17,6 +17,8 @@
package android.net.wifi;
import static org.junit.Assert.*;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
@@ -24,6 +26,9 @@
import android.net.RssiCurve;
import android.net.ScoredNetwork;
import android.net.WifiKey;
+import android.net.wifi.WifiNetworkScoreCache.CacheListener;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -33,124 +38,167 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+
/** Unit tests for {@link WifiNetworkScoreCache}. */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class WifiNetworkScoreCacheTest {
- @Mock public Context mockContext; // isn't used, can be null
- @Mock private RssiCurve mockRssiCurve;
+ public static final String SSID = "ssid";
+ public static final String FORMATTED_SSID = "\"" + SSID + "\"";
+ public static final String BSSID = "AA:AA:AA:AA:AA:AA";
- public static final String SSID = "ssid";
- public static final String FORMATTED_SSID = "\"" + SSID + "\"";
- public static final String BSSID = "AA:AA:AA:AA:AA:AA";
+ public static final WifiKey VALID_KEY = new WifiKey(FORMATTED_SSID, BSSID);
- public static final WifiKey VALID_KEY = new WifiKey(FORMATTED_SSID, BSSID);
+ public static final ScanResult VALID_SCAN_RESULT = buildScanResult(SSID, BSSID);
- public static final ScanResult VALID_SCAN_RESULT = buildScanResult(SSID, BSSID);
-
- private ScoredNetwork mValidScoredNetwork;
- private WifiNetworkScoreCache mScoreCache =
- new WifiNetworkScoreCache(mockContext);
-
- private static ScanResult buildScanResult(String ssid, String bssid) {
- return new ScanResult(
- WifiSsid.createFromAsciiEncoded(ssid),
- bssid,
- "" /* caps */,
- 0 /* level */,
- 0 /* frequency */,
- 0 /* tsf */,
- 0 /* distCm */,
- 0 /* distSdCm*/);
- }
-
- private static ScoredNetwork buildScoredNetwork(WifiKey key, RssiCurve curve) {
- return new ScoredNetwork(new NetworkKey(key), curve);
- }
-
- // Called from setup
- private void initializeCacheWithValidScoredNetwork() {
- mScoreCache.updateScores(ImmutableList.of(mValidScoredNetwork));
- }
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mValidScoredNetwork = buildScoredNetwork(VALID_KEY, mockRssiCurve);
- mScoreCache = new WifiNetworkScoreCache(mockContext);
- initializeCacheWithValidScoredNetwork();
- }
+ @Mock private Context mockApplicationContext;
+ @Mock private Context mockContext; // isn't used, can be null
+ @Mock private RssiCurve mockRssiCurve;
- @Test
- public void isScoredNetworkShouldReturnTrueAfterUpdateScoresIsCalled() {
- assertTrue(mScoreCache.isScoredNetwork(VALID_SCAN_RESULT));
- }
+ private CacheListener mCacheListener;
+ private CountDownLatch mLatch;
+ private Handler mHandler;
+ private List<ScoredNetwork> mUpdatedNetworksCaptor;
+ private ScoredNetwork mValidScoredNetwork;
+ private WifiNetworkScoreCache mScoreCache =
+ new WifiNetworkScoreCache(mockContext);
- @Test
- public void isScoredNetworkShouldReturnFalseAfterClearScoresIsCalled() {
- mScoreCache.clearScores();
- assertFalse(mScoreCache.isScoredNetwork(VALID_SCAN_RESULT));
- }
+ private static ScanResult buildScanResult(String ssid, String bssid) {
+ return new ScanResult(
+ WifiSsid.createFromAsciiEncoded(ssid),
+ bssid,
+ "" /* caps */,
+ 0 /* level */,
+ 0 /* frequency */,
+ 0 /* tsf */,
+ 0 /* distCm */,
+ 0 /* distSdCm*/);
+ }
- @Test
- public void updateScoresShouldAddNewNetwork() {
- WifiKey key2 = new WifiKey("\"ssid2\"", BSSID);
- ScoredNetwork network2 = buildScoredNetwork(key2, mockRssiCurve);
- ScanResult result2 = buildScanResult("ssid2", BSSID);
+ private static ScoredNetwork buildScoredNetwork(WifiKey key, RssiCurve curve) {
+ return new ScoredNetwork(new NetworkKey(key), curve);
+ }
- mScoreCache.updateScores(ImmutableList.of(network2));
+ // Called from setup
+ private void initializeCacheWithValidScoredNetwork() {
+ mScoreCache.updateScores(ImmutableList.of(mValidScoredNetwork));
+ }
- assertTrue(mScoreCache.isScoredNetwork(VALID_SCAN_RESULT));
- assertTrue(mScoreCache.isScoredNetwork(result2));
- }
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
- @Test
- public void hasScoreCurveShouldReturnTrue() {
- assertTrue(mScoreCache.hasScoreCurve(VALID_SCAN_RESULT));
- }
+ when(mockContext.getApplicationContext()).thenReturn(mockApplicationContext);
- @Test
- public void hasScoreCurveShouldReturnFalseWhenNoCachedNetwork() {
- ScanResult unscored = buildScanResult("fake", BSSID);
- assertFalse(mScoreCache.hasScoreCurve(unscored));
- }
+ mValidScoredNetwork = buildScoredNetwork(VALID_KEY, mockRssiCurve);
+ mScoreCache = new WifiNetworkScoreCache(mockContext);
+ initializeCacheWithValidScoredNetwork();
- @Test
- public void hasScoreCurveShouldReturnFalseWhenScoredNetworkHasNoCurve() {
- ScoredNetwork noCurve = buildScoredNetwork(VALID_KEY, null /* rssiCurve */);
- mScoreCache.updateScores(ImmutableList.of(noCurve));
+ HandlerThread thread = new HandlerThread("WifiNetworkScoreCacheTest Handler Thread");
+ thread.start();
+ mHandler = new Handler(thread.getLooper());
+ mLatch = new CountDownLatch(1);
+ mCacheListener = new CacheListener(mHandler) {
+ @Override
+ public void networkCacheUpdated(List<ScoredNetwork> updatedNetworks) {
+ mUpdatedNetworksCaptor = updatedNetworks;
+ mLatch.countDown();
+ }
+ };
+ }
- assertFalse(mScoreCache.hasScoreCurve(VALID_SCAN_RESULT));
- }
- @Test
- public void getNetworkScoreShouldReturnScore() {
- final byte score = 50;
- final int rssi = -70;
- ScanResult result = new ScanResult(VALID_SCAN_RESULT);
- result.level = rssi;
+ @Test
+ public void isScoredNetworkShouldReturnTrueAfterUpdateScoresIsCalled() {
+ assertTrue(mScoreCache.isScoredNetwork(VALID_SCAN_RESULT));
+ }
- when(mockRssiCurve.lookupScore(rssi)).thenReturn(score);
+ @Test
+ public void isScoredNetworkShouldReturnFalseAfterClearScoresIsCalled() {
+ mScoreCache.clearScores();
+ assertFalse(mScoreCache.isScoredNetwork(VALID_SCAN_RESULT));
+ }
- assertEquals(score, mScoreCache.getNetworkScore(result));
- }
+ @Test
+ public void updateScoresShouldAddNewNetwork() {
+ WifiKey key2 = new WifiKey("\"ssid2\"", BSSID);
+ ScoredNetwork network2 = buildScoredNetwork(key2, mockRssiCurve);
+ ScanResult result2 = buildScanResult("ssid2", BSSID);
- @Test
- public void getMeteredHintShouldReturnFalse() {
- assertFalse(mScoreCache.getMeteredHint(VALID_SCAN_RESULT));
- }
+ mScoreCache.updateScores(ImmutableList.of(network2));
- @Test
- public void getMeteredHintShouldReturnTrue() {
- ScoredNetwork network =
- new ScoredNetwork(new NetworkKey(VALID_KEY), mockRssiCurve, true /* metered Hint */);
- mScoreCache.updateScores(ImmutableList.of(network));
+ assertTrue(mScoreCache.isScoredNetwork(VALID_SCAN_RESULT));
+ assertTrue(mScoreCache.isScoredNetwork(result2));
+ }
- assertTrue(mScoreCache.getMeteredHint(VALID_SCAN_RESULT));
- }
+ @Test
+ public void hasScoreCurveShouldReturnTrue() {
+ assertTrue(mScoreCache.hasScoreCurve(VALID_SCAN_RESULT));
+ }
+
+ @Test
+ public void hasScoreCurveShouldReturnFalseWhenNoCachedNetwork() {
+ ScanResult unscored = buildScanResult("fake", BSSID);
+ assertFalse(mScoreCache.hasScoreCurve(unscored));
+ }
+
+ @Test
+ public void hasScoreCurveShouldReturnFalseWhenScoredNetworkHasNoCurve() {
+ ScoredNetwork noCurve = buildScoredNetwork(VALID_KEY, null /* rssiCurve */);
+ mScoreCache.updateScores(ImmutableList.of(noCurve));
+
+ assertFalse(mScoreCache.hasScoreCurve(VALID_SCAN_RESULT));
+ }
+
+ @Test
+ public void getNetworkScoreShouldReturnScore() {
+ final byte score = 50;
+ final int rssi = -70;
+ ScanResult result = new ScanResult(VALID_SCAN_RESULT);
+ result.level = rssi;
+
+ when(mockRssiCurve.lookupScore(rssi)).thenReturn(score);
+
+ assertEquals(score, mScoreCache.getNetworkScore(result));
+ }
+
+ @Test
+ public void getMeteredHintShouldReturnFalse() {
+ assertFalse(mScoreCache.getMeteredHint(VALID_SCAN_RESULT));
+ }
+
+ @Test
+ public void getMeteredHintShouldReturnTrue() {
+ ScoredNetwork network =
+ new ScoredNetwork(
+ new NetworkKey(VALID_KEY), mockRssiCurve, true /* metered Hint */);
+ mScoreCache.updateScores(ImmutableList.of(network));
+
+ assertTrue(mScoreCache.getMeteredHint(VALID_SCAN_RESULT));
+ }
+
+ @Test
+ public void updateScoresShouldInvokeCacheListener_networkCacheUpdated() {
+ mScoreCache = new WifiNetworkScoreCache(mockContext, mCacheListener);
+ initializeCacheWithValidScoredNetwork();
+
+ try {
+ mLatch.await(1, TimeUnit.SECONDS); // wait for listener to be executed
+ } catch (InterruptedException e) {
+ fail("Interrupted Exception while waiting for listener to be invoked.");
+ }
+ assertEquals("One network should be updated", 1, mUpdatedNetworksCaptor.size());
+ assertEquals(mValidScoredNetwork, mUpdatedNetworksCaptor.get(0));
+ }
}