diff options
Diffstat (limited to 'core/java/android')
30 files changed, 1130 insertions, 285 deletions
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index ffa36d6..fd40d99 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -16,6 +16,8 @@ package android.accounts; +import android.annotation.RequiresPermission; +import android.annotation.Size; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -49,6 +51,11 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static android.Manifest.permission.AUTHENTICATE_ACCOUNTS; +import static android.Manifest.permission.GET_ACCOUNTS; +import static android.Manifest.permission.MANAGE_ACCOUNTS; +import static android.Manifest.permission.USE_CREDENTIALS; + /** * This class provides access to a centralized registry of the user's * online accounts. The user enters credentials (username and password) once @@ -319,6 +326,7 @@ public class AccountManager { * @param account The account to query for a password * @return The account's password, null if none or if the account doesn't exist */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public String getPassword(final Account account) { if (account == null) throw new IllegalArgumentException("account is null"); try { @@ -344,6 +352,7 @@ public class AccountManager { * @param account The account to query for user data * @return The user data, null if the account or key doesn't exist */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public String getUserData(final Account account, final String key) { if (account == null) throw new IllegalArgumentException("account is null"); if (key == null) throw new IllegalArgumentException("key is null"); @@ -409,6 +418,7 @@ public class AccountManager { * @return An array of {@link Account}, one for each account. Empty * (never null) if no accounts have been added. */ + @RequiresPermission(GET_ACCOUNTS) public Account[] getAccounts() { try { return mService.getAccounts(null); @@ -431,6 +441,7 @@ public class AccountManager { * @return An array of {@link Account}, one for each account. Empty * (never null) if no accounts have been added. */ + @RequiresPermission(GET_ACCOUNTS) public Account[] getAccountsAsUser(int userId) { try { return mService.getAccountsAsUser(null, userId); @@ -490,6 +501,7 @@ public class AccountManager { * @return An array of {@link Account}, one per matching account. Empty * (never null) if no accounts of the specified type have been added. */ + @RequiresPermission(GET_ACCOUNTS) public Account[] getAccountsByType(String type) { return getAccountsByTypeAsUser(type, Process.myUserHandle()); } @@ -576,6 +588,7 @@ public class AccountManager { * @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) { @@ -621,6 +634,7 @@ public class AccountManager { * {@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) { @@ -659,6 +673,7 @@ public class AccountManager { * @return True if the account was successfully added, false if the account * already exists, the account is null, or another error occurs. */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public boolean addAccountExplicitly(Account account, String password, Bundle userdata) { if (account == null) throw new IllegalArgumentException("account is null"); try { @@ -684,6 +699,7 @@ public class AccountManager { * * @param account The {@link Account} to be updated. */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public boolean notifyAccountAuthenticated(Account account) { if (account == null) throw new IllegalArgumentException("account is null"); @@ -715,9 +731,10 @@ public class AccountManager { * after the name change. If successful the account's name will be the * specified new name. */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public AccountManagerFuture<Account> renameAccount( final Account account, - final String newName, + @Size(min = 1) final String newName, AccountManagerCallback<Account> callback, Handler handler) { if (account == null) throw new IllegalArgumentException("account is null."); @@ -783,6 +800,7 @@ public class AccountManager { * {@link #removeAccount(Account, Activity, AccountManagerCallback, Handler)} * instead */ + @RequiresPermission(MANAGE_ACCOUNTS) @Deprecated public AccountManagerFuture<Boolean> removeAccount(final Account account, AccountManagerCallback<Boolean> callback, Handler handler) { @@ -837,6 +855,7 @@ public class AccountManager { * adding accounts (of this type) has been disabled by policy * </ul> */ + @RequiresPermission(MANAGE_ACCOUNTS) public AccountManagerFuture<Bundle> removeAccount(final Account account, final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { if (account == null) throw new IllegalArgumentException("account is null"); @@ -909,6 +928,7 @@ public class AccountManager { * account did not exist, the account is null, or another error * occurs. */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public boolean removeAccountExplicitly(Account account) { if (account == null) throw new IllegalArgumentException("account is null"); try { @@ -935,6 +955,7 @@ public class AccountManager { * @param accountType The account type of the auth token to invalidate, must not be null * @param authToken The auth token to invalidate, may be null */ + @RequiresPermission(anyOf = {MANAGE_ACCOUNTS, USE_CREDENTIALS}) public void invalidateAuthToken(final String accountType, final String authToken) { if (accountType == null) throw new IllegalArgumentException("accountType is null"); try { @@ -964,6 +985,7 @@ public class AccountManager { * @return The cached auth token for this account and type, or null if * no auth token is cached or the account does not exist. */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public String peekAuthToken(final Account account, final String authTokenType) { if (account == null) throw new IllegalArgumentException("account is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); @@ -990,6 +1012,7 @@ public class AccountManager { * @param account The account to set a password for * @param password The password to set, null to clear the password */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public void setPassword(final Account account, final String password) { if (account == null) throw new IllegalArgumentException("account is null"); try { @@ -1014,6 +1037,7 @@ public class AccountManager { * * @param account The account whose password to clear */ + @RequiresPermission(MANAGE_ACCOUNTS) public void clearPassword(final Account account) { if (account == null) throw new IllegalArgumentException("account is null"); try { @@ -1039,6 +1063,7 @@ public class AccountManager { * @param key The userdata key to set. Must not be null * @param value The value to set, null to clear this userdata key */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public void setUserData(final Account account, final String key, final String value) { if (account == null) throw new IllegalArgumentException("account is null"); if (key == null) throw new IllegalArgumentException("key is null"); @@ -1066,6 +1091,7 @@ public class AccountManager { * @param authTokenType The type of the auth token, see {#getAuthToken} * @param authToken The auth token to add to the cache */ + @RequiresPermission(AUTHENTICATE_ACCOUNTS) public void setAuthToken(Account account, final String authTokenType, final String authToken) { if (account == null) throw new IllegalArgumentException("account is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); @@ -1100,6 +1126,7 @@ public class AccountManager { * @throws java.io.IOException if the authenticator experienced an I/O problem * creating a new auth token, usually because of network trouble */ + @RequiresPermission(USE_CREDENTIALS) public String blockingGetAuthToken(Account account, String authTokenType, boolean notifyAuthFailure) throws OperationCanceledException, IOException, AuthenticatorException { @@ -1174,6 +1201,7 @@ public class AccountManager { * authenticator-dependent. The caller should verify the validity of the * account before requesting an auth token. */ + @RequiresPermission(USE_CREDENTIALS) public AccountManagerFuture<Bundle> getAuthToken( final Account account, final String authTokenType, final Bundle options, final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { @@ -1264,6 +1292,7 @@ public class AccountManager { * boolean, AccountManagerCallback, android.os.Handler)} instead */ @Deprecated + @RequiresPermission(USE_CREDENTIALS) public AccountManagerFuture<Bundle> getAuthToken( final Account account, final String authTokenType, final boolean notifyAuthFailure, @@ -1342,6 +1371,7 @@ public class AccountManager { * authenticator-dependent. The caller should verify the validity of the * account before requesting an auth token. */ + @RequiresPermission(USE_CREDENTIALS) public AccountManagerFuture<Bundle> getAuthToken( final Account account, final String authTokenType, final Bundle options, final boolean notifyAuthFailure, @@ -1411,6 +1441,7 @@ public class AccountManager { * creating a new account, usually because of network trouble * </ul> */ + @RequiresPermission(MANAGE_ACCOUNTS) public AccountManagerFuture<Bundle> addAccount(final String accountType, final String authTokenType, final String[] requiredFeatures, final Bundle addAccountOptions, @@ -1598,6 +1629,7 @@ public class AccountManager { * verifying the password, usually because of network trouble * </ul> */ + @RequiresPermission(MANAGE_ACCOUNTS) public AccountManagerFuture<Bundle> confirmCredentials(final Account account, final Bundle options, final Activity activity, @@ -1674,6 +1706,7 @@ public class AccountManager { * verifying the password, usually because of network trouble * </ul> */ + @RequiresPermission(MANAGE_ACCOUNTS) public AccountManagerFuture<Bundle> updateCredentials(final Account account, final String authTokenType, final Bundle options, final Activity activity, @@ -1725,6 +1758,7 @@ public class AccountManager { * updating settings, usually because of network trouble * </ul> */ + @RequiresPermission(MANAGE_ACCOUNTS) public AccountManagerFuture<Bundle> editProperties(final String accountType, final Activity activity, final AccountManagerCallback<Bundle> callback, final Handler handler) { @@ -2258,6 +2292,7 @@ public class AccountManager { * updating settings, usually because of network trouble * </ul> */ + @RequiresPermission(MANAGE_ACCOUNTS) public AccountManagerFuture<Bundle> getAuthTokenByFeatures( final String accountType, final String authTokenType, final String[] features, final Activity activity, final Bundle addAccountOptions, @@ -2382,6 +2417,7 @@ public class AccountManager { * @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) { diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 2e45b79..da6d8c5 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1638,6 +1638,12 @@ public final class ActivityThread { return sCurrentActivityThread; } + public static String currentOpPackageName() { + ActivityThread am = currentActivityThread(); + return (am != null && am.getApplication() != null) + ? am.getApplication().getOpPackageName() : null; + } + public static String currentPackageName() { ActivityThread am = currentActivityThread(); return (am != null && am.mBoundApplication != null) diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 8a3c9c8..6bbbf9e 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -223,8 +223,12 @@ public class AppOpsManager { public static final int OP_PROCESS_OUTGOING_CALLS = 54; /** @hide User the fingerprint API. */ public static final int OP_USE_FINGERPRINT = 55; + /** @hide Access to body sensors such as heart rate, etc. */ + public static final int OP_BODY_SENSORS = 56; + /** @hide Read previously received cell broadcast messages. */ + public static final int OP_READ_CELL_BROADCASTS = 57; /** @hide */ - public static final int _NUM_OP = 56; + public static final int _NUM_OP = 58; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -280,9 +284,6 @@ public class AppOpsManager { /** @hide Allows an application to send SMS messages. */ public static final String OPSTR_SEND_SMS = "android:send_sms"; - /** @hide Allows an application to add system alert windows. */ - public static final String OPSTR_SYSTEM_ALERT_WINDOW - = "android:system_alert_window"; /** @hide Required to be able to access the camera device. */ public static final String OPSTR_CAMERA = "android:camera"; @@ -295,6 +296,18 @@ public class AppOpsManager { /** @hide Required to access phone state related information. */ public static final String OPSTR_ADD_VOICEMAIL = "android:add_voicemail"; + /** @hide Access APIs for SIP calling over VOIP or WiFi */ + public static final String OPSTR_USE_SIP + = "android:use_sip"; + /** @hide Use the fingerprint API. */ + public static final String OPSTR_USE_FINGERPRINT + = "android:use_fingerprint"; + /** @hide Access to body sensors such as heart rate, etc. */ + public static final String OPSTR_BODY_SENSORS + = "android:body_sensors"; + /** @hide Read previously received cell broadcast messages. */ + public static final String OPSTR_READ_CELL_BROADCASTS + = "android:read_cell_broadcasts"; /** * This maps each operation to the operation that serves as the @@ -360,7 +373,9 @@ public class AppOpsManager { OP_ADD_VOICEMAIL, OP_USE_SIP, OP_PROCESS_OUTGOING_CALLS, - OP_USE_FINGERPRINT + OP_USE_FINGERPRINT, + OP_BODY_SENSORS, + OP_READ_CELL_BROADCASTS }; /** @@ -372,30 +387,30 @@ public class AppOpsManager { OPSTR_FINE_LOCATION, null, null, + OPSTR_READ_CONTACTS, + OPSTR_WRITE_CONTACTS, + OPSTR_READ_CALL_LOG, + OPSTR_WRITE_CALL_LOG, + OPSTR_READ_CALENDAR, + OPSTR_WRITE_CALENDAR, null, null, null, + OPSTR_CALL_PHONE, + OPSTR_READ_SMS, null, + OPSTR_RECEIVE_SMS, null, + OPSTR_RECEIVE_MMS, + OPSTR_RECEIVE_WAP_PUSH, + OPSTR_SEND_SMS, null, null, null, null, null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, + OPSTR_CAMERA, + OPSTR_RECORD_AUDIO, null, null, null, @@ -419,11 +434,13 @@ public class AppOpsManager { null, null, null, + OPSTR_READ_PHONE_STATE, + OPSTR_ADD_VOICEMAIL, + OPSTR_USE_SIP, null, - null, - null, - null, - null + OPSTR_USE_FINGERPRINT, + OPSTR_BODY_SENSORS, + OPSTR_READ_CELL_BROADCASTS }; /** @@ -486,7 +503,9 @@ public class AppOpsManager { "ADD_VOICEMAIL", "USE_SIP", "PROCESS_OUTGOING_CALLS", - "USE_FINGERPRINT" + "USE_FINGERPRINT", + "BODY_SENSORS", + "READ_CELL_BROADCASTS" }; /** @@ -549,7 +568,9 @@ public class AppOpsManager { Manifest.permission.ADD_VOICEMAIL, Manifest.permission.USE_SIP, Manifest.permission.PROCESS_OUTGOING_CALLS, - Manifest.permission.USE_FINGERPRINT + Manifest.permission.USE_FINGERPRINT, + Manifest.permission.BODY_SENSORS, + Manifest.permission.READ_CELL_BROADCASTS }; /** @@ -613,7 +634,9 @@ public class AppOpsManager { null, // ADD_VOICEMAIL null, // USE_SIP null, // PROCESS_OUTGOING_CALLS - null // USE_FINGERPRINT + null, // USE_FINGERPRINT + null, // BODY_SENSORS + null // READ_CELL_BROADCASTS }; /** @@ -676,7 +699,9 @@ public class AppOpsManager { false, //ADD_VOICEMAIL false, // USE_SIP false, // PROCESS_OUTGOING_CALLS - false // USE_FINGERPRINT + false, // USE_FINGERPRINT + false, // BODY_SENSORS + false // READ_CELL_BROADCASTS }; /** @@ -738,6 +763,8 @@ public class AppOpsManager { AppOpsManager.MODE_ALLOWED, AppOpsManager.MODE_ALLOWED, AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_ALLOWED, AppOpsManager.MODE_ALLOWED }; @@ -804,37 +831,20 @@ public class AppOpsManager { false, false, false, + false, + false, false }; /** - * This is a mapping from a permission name to public app op name. + * Mapping from an app op name to the app op code. */ - private static final ArrayMap<String, String> sPermToOp = new ArrayMap<>(); - static { - sPermToOp.put(Manifest.permission.ACCESS_COARSE_LOCATION, OPSTR_COARSE_LOCATION); - sPermToOp.put(Manifest.permission.ACCESS_FINE_LOCATION, OPSTR_FINE_LOCATION); - sPermToOp.put(Manifest.permission.PACKAGE_USAGE_STATS, OPSTR_GET_USAGE_STATS); - sPermToOp.put(Manifest.permission.READ_CONTACTS, OPSTR_READ_CONTACTS); - sPermToOp.put(Manifest.permission.WRITE_CONTACTS, OPSTR_WRITE_CONTACTS); - sPermToOp.put(Manifest.permission.READ_CALL_LOG, OPSTR_READ_CALL_LOG); - sPermToOp.put(Manifest.permission.WRITE_CALL_LOG, OPSTR_WRITE_CALL_LOG); - sPermToOp.put(Manifest.permission.READ_CALENDAR, OPSTR_READ_CALENDAR); - sPermToOp.put(Manifest.permission.WRITE_CALENDAR, OPSTR_WRITE_CALENDAR); - sPermToOp.put(Manifest.permission.CALL_PHONE, OPSTR_CALL_PHONE); - sPermToOp.put(Manifest.permission.READ_SMS, OPSTR_READ_SMS); - sPermToOp.put(Manifest.permission.RECEIVE_SMS, OPSTR_RECEIVE_SMS); - sPermToOp.put(Manifest.permission.RECEIVE_MMS, OPSTR_RECEIVE_MMS); - sPermToOp.put(Manifest.permission.RECEIVE_WAP_PUSH, OPSTR_RECEIVE_WAP_PUSH); - sPermToOp.put(Manifest.permission.SEND_SMS, OPSTR_SEND_SMS); - sPermToOp.put(Manifest.permission.SYSTEM_ALERT_WINDOW, OPSTR_SYSTEM_ALERT_WINDOW); - sPermToOp.put(Manifest.permission.CAMERA, OPSTR_CAMERA); - sPermToOp.put(Manifest.permission.RECORD_AUDIO, OPSTR_RECORD_AUDIO); - sPermToOp.put(Manifest.permission.READ_PHONE_STATE, OPSTR_READ_PHONE_STATE); - sPermToOp.put(Manifest.permission.ADD_VOICEMAIL, OPSTR_ADD_VOICEMAIL); - } + private static HashMap<String, Integer> sOpStrToOp = new HashMap<>(); - private static HashMap<String, Integer> sOpStrToOp = new HashMap<String, Integer>(); + /** + * Mapping from a permission to the corresponding app op. + */ + private static HashMap<String, Integer> sPermToOp = new HashMap<>(); static { if (sOpToSwitch.length != _NUM_OP) { @@ -874,6 +884,11 @@ public class AppOpsManager { sOpStrToOp.put(sOpToString[i], i); } } + for (int i=0; i<_NUM_OP; i++) { + if (sOpPerms[i] != null) { + sPermToOp.put(sOpPerms[i], i); + } + } } /** @@ -922,6 +937,14 @@ public class AppOpsManager { } /** + * Retrieve the app op code for a permission, or null if there is not one. + * @hide + */ + public static int permissionToOpCode(String permission) { + return sPermToOp.get(permission); + } + + /** * Retrieve whether the op allows the system (and system ui) to * bypass the user restriction. * @hide @@ -1185,7 +1208,11 @@ public class AppOpsManager { */ @SystemApi public static String permissionToOp(String permission) { - return sPermToOp.get(permission); + final Integer opCode = sPermToOp.get(permission); + if (opCode == null) { + return null; + } + return sOpToString[opCode]; } /** diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index d8556a2..6fca0de 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -33,15 +33,21 @@ import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.system.StructStat; +import android.util.ArraySet; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.util.HashSet; +import java.util.Collection; import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; +import org.xmlpull.v1.XmlPullParserException; + /** * Provides the central interface between an * application and Android's data backup infrastructure. An application that wishes @@ -164,7 +170,6 @@ public abstract class BackupAgent extends ContextWrapper { * to do one-time initialization before the actual backup or restore operation * is begun. * <p> - * Agents do not need to override this method. */ public void onCreate() { } @@ -268,19 +273,41 @@ public abstract class BackupAgent extends ContextWrapper { * listed above. Apps only need to override this method if they need to impose special * limitations on which files are being stored beyond the control that * {@link #getNoBackupFilesDir()} offers. + * Alternatively they can provide an xml resource to specify what data to include or exclude. + * * * @param data A structured wrapper pointing to the backup destination. * @throws IOException * * @see Context#getNoBackupFilesDir() + * @see ApplicationInfo#fullBackupContent * @see #fullBackupFile(File, FullBackupDataOutput) * @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) */ public void onFullBackup(FullBackupDataOutput data) throws IOException { - ApplicationInfo appInfo = getApplicationInfo(); + FullBackup.BackupScheme backupScheme = FullBackup.getBackupScheme(this); + if (!backupScheme.isFullBackupContentEnabled()) { + return; + } + + Map<String, Set<String>> manifestIncludeMap; + ArraySet<String> manifestExcludeSet; + try { + manifestIncludeMap = + backupScheme.maybeParseAndGetCanonicalIncludePaths(); + manifestExcludeSet = backupScheme.maybeParseAndGetCanonicalExcludePaths(); + } catch (IOException | XmlPullParserException e) { + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "Exception trying to parse fullBackupContent xml file!" + + " Aborting full backup.", e); + } + return; + } + + final String packageName = getPackageName(); + final ApplicationInfo appInfo = getApplicationInfo(); - // Note that we don't need to think about the no_backup dir because it's outside - // all of the ones we will be traversing String rootDir = new File(appInfo.dataDir).getCanonicalPath(); String filesDir = getFilesDir().getCanonicalPath(); String nobackupDir = getNoBackupFilesDir().getCanonicalPath(); @@ -292,34 +319,49 @@ public abstract class BackupAgent extends ContextWrapper { ? new File(appInfo.nativeLibraryDir).getCanonicalPath() : null; - // Filters, the scan queue, and the set of resulting entities - HashSet<String> filterSet = new HashSet<String>(); - String packageName = getPackageName(); + // Maintain a set of excluded directories so that as we traverse the tree we know we're not + // going places we don't expect, and so the manifest includes can't take precedence over + // what the framework decides is not to be included. + final ArraySet<String> traversalExcludeSet = new ArraySet<String>(); - // Okay, start with the app's root tree, but exclude all of the canonical subdirs + // Add the directories we always exclude. + traversalExcludeSet.add(cacheDir); + traversalExcludeSet.add(codeCacheDir); + traversalExcludeSet.add(nobackupDir); if (libDir != null) { - filterSet.add(libDir); + traversalExcludeSet.add(libDir); } - filterSet.add(cacheDir); - filterSet.add(codeCacheDir); - filterSet.add(databaseDir); - filterSet.add(sharedPrefsDir); - filterSet.add(filesDir); - filterSet.add(nobackupDir); - fullBackupFileTree(packageName, FullBackup.ROOT_TREE_TOKEN, rootDir, filterSet, data); - - // Now do the same for the files dir, db dir, and shared prefs dir - filterSet.add(rootDir); - filterSet.remove(filesDir); - fullBackupFileTree(packageName, FullBackup.DATA_TREE_TOKEN, filesDir, filterSet, data); - - filterSet.add(filesDir); - filterSet.remove(databaseDir); - fullBackupFileTree(packageName, FullBackup.DATABASE_TREE_TOKEN, databaseDir, filterSet, data); - - filterSet.add(databaseDir); - filterSet.remove(sharedPrefsDir); - fullBackupFileTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, sharedPrefsDir, filterSet, data); + + traversalExcludeSet.add(databaseDir); + traversalExcludeSet.add(sharedPrefsDir); + traversalExcludeSet.add(filesDir); + + // Root dir first. + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.ROOT_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(rootDir); + + // Data dir next. + traversalExcludeSet.remove(filesDir); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.DATA_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(filesDir); + + // Database directory. + traversalExcludeSet.remove(databaseDir); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.DATABASE_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(databaseDir); + + // SharedPrefs. + traversalExcludeSet.remove(sharedPrefsDir); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(sharedPrefsDir); // getExternalFilesDir() location associated with this app. Technically there should // not be any files here if the app does not properly have permission to access @@ -331,8 +373,36 @@ public abstract class BackupAgent extends ContextWrapper { if (Process.myUid() != Process.SYSTEM_UID) { File efLocation = getExternalFilesDir(null); if (efLocation != null) { - fullBackupFileTree(packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, - efLocation.getCanonicalPath(), null, data); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + } + + } + } + + /** + * Check whether the xml yielded any <include/> tag for the provided <code>domainToken</code>. + * If so, perform a {@link #fullBackupFileTree} which backs up the file or recurses if the path + * is a directory. + */ + private void applyXmlFiltersAndDoFullBackupForDomain(String packageName, String domainToken, + Map<String, Set<String>> includeMap, + ArraySet<String> filterSet, + ArraySet<String> traversalExcludeSet, + FullBackupDataOutput data) + throws IOException { + if (includeMap == null || includeMap.size() == 0) { + // Do entire sub-tree for the provided token. + fullBackupFileTree(packageName, domainToken, + FullBackup.getBackupScheme(this).tokenToDirectoryPath(domainToken), + filterSet, traversalExcludeSet, data); + } else if (includeMap.get(domainToken) != null) { + // This will be null if the xml parsing didn't yield any rules for + // this domain (there may still be rules for other domains). + for (String includeFile : includeMap.get(domainToken)) { + fullBackupFileTree(packageName, domainToken, includeFile, filterSet, + traversalExcludeSet, data); } } } @@ -430,21 +500,31 @@ public abstract class BackupAgent extends ContextWrapper { // without transmitting any file data. if (DEBUG) Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain + " rootpath=" + rootpath); - + FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, output); } /** * Scan the dir tree (if it actually exists) and process each entry we find. If the - * 'excludes' parameter is non-null, it is consulted each time a new file system entity + * 'excludes' parameters are non-null, they are consulted each time a new file system entity * is visited to see whether that entity (and its subtree, if appropriate) should be * omitted from the backup process. * + * @param systemExcludes An optional list of excludes. * @hide */ - protected final void fullBackupFileTree(String packageName, String domain, String rootPath, - HashSet<String> excludes, FullBackupDataOutput output) { - File rootFile = new File(rootPath); + protected final void fullBackupFileTree(String packageName, String domain, String startingPath, + ArraySet<String> manifestExcludes, + ArraySet<String> systemExcludes, + FullBackupDataOutput output) { + // Pull out the domain and set it aside to use when making the tarball. + String domainPath = FullBackup.getBackupScheme(this).tokenToDirectoryPath(domain); + if (domainPath == null) { + // Should never happen. + return; + } + + File rootFile = new File(startingPath); if (rootFile.exists()) { LinkedList<File> scanQueue = new LinkedList<File>(); scanQueue.add(rootFile); @@ -456,7 +536,10 @@ public abstract class BackupAgent extends ContextWrapper { filePath = file.getCanonicalPath(); // prune this subtree? - if (excludes != null && excludes.contains(filePath)) { + if (manifestExcludes != null && manifestExcludes.contains(filePath)) { + continue; + } + if (systemExcludes != null && systemExcludes.contains(filePath)) { continue; } @@ -475,14 +558,20 @@ public abstract class BackupAgent extends ContextWrapper { } } catch (IOException e) { if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file); + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, "Error canonicalizing path of " + file); + } continue; } catch (ErrnoException e) { if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, "Error scanning file " + file + " : " + e); + } continue; } // Finally, back this file up (or measure it) before proceeding - FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, output); + FullBackup.backupToTar(packageName, domain, null, domainPath, filePath, output); } } } @@ -516,10 +605,91 @@ public abstract class BackupAgent extends ContextWrapper { public void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException { + FullBackup.BackupScheme bs = FullBackup.getBackupScheme(this); + if (!bs.isFullBackupContentEnabled()) { + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "onRestoreFile \"" + destination.getCanonicalPath() + + "\" : fullBackupContent not enabled for " + getPackageName()); + } + return; + } + Map<String, Set<String>> includes = null; + ArraySet<String> excludes = null; + final String destinationCanonicalPath = destination.getCanonicalPath(); + try { + includes = bs.maybeParseAndGetCanonicalIncludePaths(); + excludes = bs.maybeParseAndGetCanonicalExcludePaths(); + } catch (XmlPullParserException e) { + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "onRestoreFile \"" + destinationCanonicalPath + + "\" : Exception trying to parse fullBackupContent xml file!" + + " Aborting onRestoreFile.", e); + } + return; + } + + if (excludes != null && + isFileSpecifiedInPathList(destination, excludes)) { + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "onRestoreFile: \"" + destinationCanonicalPath + "\": listed in" + + " excludes; skipping."); + } + return; + } + + if (includes != null && !includes.isEmpty()) { + // Rather than figure out the <include/> domain based on the path (a lot of code, and + // it's a small list), we'll go through and look for it. + boolean explicitlyIncluded = false; + for (Set<String> domainIncludes : includes.values()) { + explicitlyIncluded |= isFileSpecifiedInPathList(destination, domainIncludes); + if (explicitlyIncluded) { + break; + } + } + if (!explicitlyIncluded) { + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "onRestoreFile: Trying to restore \"" + + destinationCanonicalPath + "\" but it isn't specified" + + " in the included files; skipping."); + } + return; + } + } FullBackup.restoreFile(data, size, type, mode, mtime, destination); } /** + * @return True if the provided file is either directly in the provided list, or the provided + * file is within a directory in the list. + */ + private boolean isFileSpecifiedInPathList(File file, Collection<String> canonicalPathList) + throws IOException { + for (String canonicalPath : canonicalPathList) { + File fileFromList = new File(canonicalPath); + if (fileFromList.isDirectory()) { + if (file.isDirectory()) { + // If they are both directories check exact equals. + return file.equals(fileFromList); + } else { + // O/w we have to check if the file is within the directory from the list. + return file.getCanonicalPath().startsWith(canonicalPath); + } + } else { + if (file.equals(fileFromList)) { + // Need to check the explicit "equals" so we don't end up with substrings. + return true; + } + } + } + return false; + } + + /** * Only specialized platform agents should overload this entry point to support * restores to crazy non-app locations. * @hide @@ -533,31 +703,9 @@ public abstract class BackupAgent extends ContextWrapper { + " domain=" + domain + " relpath=" + path + " mode=" + mode + " mtime=" + mtime); - // Parse out the semantic domains into the correct physical location - if (domain.equals(FullBackup.DATA_TREE_TOKEN)) { - basePath = getFilesDir().getCanonicalPath(); - } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) { - basePath = getDatabasePath("foo").getParentFile().getCanonicalPath(); - } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { - basePath = new File(getApplicationInfo().dataDir).getCanonicalPath(); - } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { - basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); - } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) { - basePath = getCacheDir().getCanonicalPath(); - } else if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { - // make sure we can try to restore here before proceeding - if (Process.myUid() != Process.SYSTEM_UID) { - File efLocation = getExternalFilesDir(null); - if (efLocation != null) { - basePath = getExternalFilesDir(null).getCanonicalPath(); - mode = -1; // < 0 is a token to skip attempting a chmod() - } - } - } else if (domain.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) { - basePath = getNoBackupFilesDir().getCanonicalPath(); - } else { - // Not a supported location - Log.i(TAG, "Unrecognized domain " + domain); + basePath = FullBackup.getBackupScheme(this).tokenToDirectoryPath(domain); + if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { + mode = -1; // < 0 is a token to skip attempting a chmod() } // Now that we've figured out where the data goes, send it on its way diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index 259884e..7718a36 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -16,16 +16,31 @@ package android.app.backup; -import android.os.ParcelFileDescriptor; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.os.*; +import android.os.Process; import android.system.ErrnoException; import android.system.Os; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + +import org.xmlpull.v1.XmlPullParser; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.xmlpull.v1.XmlPullParserException; /** * Global constant definitions et cetera related to the full-backup-to-fd * binary format. Nothing in this namespace is part of any API; it's all @@ -35,6 +50,8 @@ import java.io.IOException; */ public class FullBackup { static final String TAG = "FullBackup"; + /** Enable this log tag to get verbose information while parsing the client xml. */ + static final String TAG_XML_PARSER = "BackupXmlParserLogging"; public static final String APK_TREE_TOKEN = "a"; public static final String OBB_TREE_TOKEN = "obb"; @@ -60,6 +77,27 @@ public class FullBackup { static public native int backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output); + private static final Map<String, BackupScheme> kPackageBackupSchemeMap = + new ArrayMap<String, BackupScheme>(); + + static synchronized BackupScheme getBackupScheme(Context context) { + BackupScheme backupSchemeForPackage = + kPackageBackupSchemeMap.get(context.getPackageName()); + if (backupSchemeForPackage == null) { + backupSchemeForPackage = new BackupScheme(context); + kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage); + } + return backupSchemeForPackage; + } + + public static BackupScheme getBackupSchemeForTest(Context context) { + BackupScheme testing = new BackupScheme(context); + testing.mExcludes = new ArraySet(); + testing.mIncludes = new ArrayMap(); + return testing; + } + + /** * Copy data from a socket to the given File location on permanent storage. The * modification time and access mode of the resulting file will be set if desired, @@ -106,6 +144,8 @@ public class FullBackup { if (!parent.exists()) { // in practice this will only be for the default semantic directories, // and using the default mode for those is appropriate. + // This can also happen for the case where a parent directory has been + // excluded, but a file within that directory has been included. parent.mkdirs(); } out = new FileOutputStream(outFile); @@ -154,4 +194,363 @@ public class FullBackup { outFile.setLastModified(mtime); } } + + @VisibleForTesting + public static class BackupScheme { + private final File FILES_DIR; + private final File DATABASE_DIR; + private final File ROOT_DIR; + private final File SHAREDPREF_DIR; + private final File EXTERNAL_DIR; + private final File CACHE_DIR; + private final File NOBACKUP_DIR; + + final int mFullBackupContent; + final PackageManager mPackageManager; + final String mPackageName; + + /** + * Parse out the semantic domains into the correct physical location. + */ + String tokenToDirectoryPath(String domainToken) { + try { + if (domainToken.equals(FullBackup.DATA_TREE_TOKEN)) { + return FILES_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) { + return DATABASE_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) { + return ROOT_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { + return SHAREDPREF_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) { + return CACHE_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { + if (EXTERNAL_DIR != null) { + return EXTERNAL_DIR.getCanonicalPath(); + } else { + return null; + } + } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) { + return NOBACKUP_DIR.getCanonicalPath(); + } + // Not a supported location + Log.i(TAG, "Unrecognized domain " + domainToken); + return null; + } catch (IOException e) { + Log.i(TAG, "Error reading directory for domain: " + domainToken); + return null; + } + + } + /** + * A map of domain -> list of canonical file names in that domain that are to be included. + * We keep track of the domain so that we can go through the file system in order later on. + */ + Map<String, Set<String>> mIncludes; + /**e + * List that will be populated with the canonical names of each file or directory that is + * to be excluded. + */ + ArraySet<String> mExcludes; + + BackupScheme(Context context) { + mFullBackupContent = context.getApplicationInfo().fullBackupContent; + mPackageManager = context.getPackageManager(); + mPackageName = context.getPackageName(); + FILES_DIR = context.getFilesDir(); + DATABASE_DIR = context.getDatabasePath("foo").getParentFile(); + ROOT_DIR = new File(context.getApplicationInfo().dataDir); + SHAREDPREF_DIR = context.getSharedPrefsFile("foo").getParentFile(); + CACHE_DIR = context.getCacheDir(); + NOBACKUP_DIR = context.getNoBackupFilesDir(); + if (android.os.Process.myUid() != Process.SYSTEM_UID) { + EXTERNAL_DIR = context.getExternalFilesDir(null); + } else { + EXTERNAL_DIR = null; + } + } + + boolean isFullBackupContentEnabled() { + if (mFullBackupContent < 0) { + // android:fullBackupContent="false", bail. + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\""); + } + return false; + } + return true; + } + + /** + * @return A mapping of domain -> canonical paths within that domain. Each of these paths + * specifies a file that the client has explicitly included in their backup set. If this + * map is empty we will back up the entire data directory (including managed external + * storage). + */ + public synchronized Map<String, Set<String>> maybeParseAndGetCanonicalIncludePaths() + throws IOException, XmlPullParserException { + if (mIncludes == null) { + maybeParseBackupSchemeLocked(); + } + return mIncludes; + } + + /** + * @return A set of canonical paths that are to be excluded from the backup/restore set. + */ + public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths() + throws IOException, XmlPullParserException { + if (mExcludes == null) { + maybeParseBackupSchemeLocked(); + } + return mExcludes; + } + + private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException { + // This not being null is how we know that we've tried to parse the xml already. + mIncludes = new ArrayMap<String, Set<String>>(); + mExcludes = new ArraySet<String>(); + + if (mFullBackupContent == 0) { + // android:fullBackupContent="true" which means that we'll do everything. + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\""); + } + } else { + // android:fullBackupContent="@xml/some_resource". + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "android:fullBackupContent - found xml resource"); + } + XmlResourceParser parser = null; + try { + parser = mPackageManager + .getResourcesForApplication(mPackageName) + .getXml(mFullBackupContent); + parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes); + } catch (PackageManager.NameNotFoundException e) { + // Throw it as an IOException + throw new IOException(e); + } finally { + if (parser != null) { + parser.close(); + } + } + } + } + + @VisibleForTesting + public void parseBackupSchemeFromXmlLocked(XmlPullParser parser, + Set<String> excludes, + Map<String, Set<String>> includes) + throws IOException, XmlPullParserException { + int event = parser.getEventType(); // START_DOCUMENT + while (event != XmlPullParser.START_TAG) { + event = parser.next(); + } + + if (!"full-backup-content".equals(parser.getName())) { + throw new XmlPullParserException("Xml file didn't start with correct tag" + + " (<full-backup-content>). Found \"" + parser.getName() + "\""); + } + + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "\n"); + Log.v(TAG_XML_PARSER, "===================================================="); + Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource."); + Log.v(TAG_XML_PARSER, "===================================================="); + Log.v(TAG_XML_PARSER, ""); + } + + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + switch (event) { + case XmlPullParser.START_TAG: + validateInnerTagContents(parser); + final String domainFromXml = parser.getAttributeValue(null, "domain"); + final File domainDirectory = + getDirectoryForCriteriaDomain(domainFromXml); + if (domainDirectory == null) { + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": " + + "domain=\"" + domainFromXml + "\" invalid; skipping"); + } + break; + } + final File canonicalFile = + extractCanonicalFile(domainDirectory, + parser.getAttributeValue(null, "path")); + if (canonicalFile == null) { + break; + } + + Set<String> activeSet = parseCurrentTagForDomain( + parser, excludes, includes, domainFromXml); + activeSet.add(canonicalFile.getCanonicalPath()); + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath() + + " for domain \"" + domainFromXml + "\""); + } + + // Special case journal files (not dirs) for sqlite database. frowny-face. + // Note that for a restore, the file is never a directory (b/c it doesn't + // exist). We have no way of knowing a priori whether or not to expect a + // dir, so we add the -journal anyway to be safe. + if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) { + final String canonicalJournalPath = + canonicalFile.getCanonicalPath() + "-journal"; + activeSet.add(canonicalJournalPath); + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...automatically generated " + + canonicalJournalPath + ". Ignore if nonexistant."); + } + } + } + } + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "\n"); + Log.v(TAG_XML_PARSER, "Xml resource parsing complete."); + Log.v(TAG_XML_PARSER, "Final tally."); + Log.v(TAG_XML_PARSER, "Includes:"); + if (includes.isEmpty()) { + Log.v(TAG_XML_PARSER, " ...nothing specified (This means the entirety of app" + + " data minus excludes)"); + } else { + for (Map.Entry<String, Set<String>> entry : includes.entrySet()) { + Log.v(TAG_XML_PARSER, " domain=" + entry.getKey()); + for (String includeData : entry.getValue()) { + Log.v(TAG_XML_PARSER, " " + includeData); + } + } + } + + Log.v(TAG_XML_PARSER, "Excludes:"); + if (excludes.isEmpty()) { + Log.v(TAG_XML_PARSER, " ...nothing to exclude."); + } else { + for (String excludeData : excludes) { + Log.v(TAG_XML_PARSER, " " + excludeData); + } + } + + Log.v(TAG_XML_PARSER, " "); + Log.v(TAG_XML_PARSER, "===================================================="); + Log.v(TAG_XML_PARSER, "\n"); + } + } + + private Set<String> parseCurrentTagForDomain(XmlPullParser parser, + Set<String> excludes, + Map<String, Set<String>> includes, + String domain) + throws XmlPullParserException { + if ("include".equals(parser.getName())) { + final String domainToken = getTokenForXmlDomain(domain); + Set<String> includeSet = includes.get(domainToken); + if (includeSet == null) { + includeSet = new ArraySet<String>(); + includes.put(domainToken, includeSet); + } + return includeSet; + } else if ("exclude".equals(parser.getName())) { + return excludes; + } else { + // Unrecognised tag => hard failure. + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "Invalid tag found in xml \"" + + parser.getName() + "\"; aborting operation."); + } + throw new XmlPullParserException("Unrecognised tag in backup" + + " criteria xml (" + parser.getName() + ")"); + } + } + + /** + * Map xml specified domain (human-readable, what clients put in their manifest's xml) to + * BackupAgent internal data token. + * @return null if the xml domain was invalid. + */ + private String getTokenForXmlDomain(String xmlDomain) { + if ("root".equals(xmlDomain)) { + return FullBackup.ROOT_TREE_TOKEN; + } else if ("file".equals(xmlDomain)) { + return FullBackup.DATA_TREE_TOKEN; + } else if ("database".equals(xmlDomain)) { + return FullBackup.DATABASE_TREE_TOKEN; + } else if ("sharedpref".equals(xmlDomain)) { + return FullBackup.SHAREDPREFS_TREE_TOKEN; + } else if ("external".equals(xmlDomain)) { + return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN; + } else { + return null; + } + } + + /** + * + * @param domain Directory where the specified file should exist. Not null. + * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may be + * null. + * @return The canonical path of the file specified or null if no such file exists. + */ + private File extractCanonicalFile(File domain, String filePathFromXml) { + if (filePathFromXml == null) { + // Allow things like <include domain="sharedpref"/> + filePathFromXml = ""; + } + if (filePathFromXml.contains("..")) { + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml + + "\", but the \"..\" path is not permitted; skipping."); + } + return null; + } + if (filePathFromXml.contains("//")) { + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml + + "\", which contains the invalid \"//\" sequence; skipping."); + } + return null; + } + return new File(domain, filePathFromXml); + } + + /** + * @param domain parsed from xml. Not sanitised before calling this function so may be null. + * @return The directory relevant to the domain specified. + */ + private File getDirectoryForCriteriaDomain(String domain) { + if (TextUtils.isEmpty(domain)) { + return null; + } + if ("file".equals(domain)) { + return FILES_DIR; + } else if ("database".equals(domain)) { + return DATABASE_DIR; + } else if ("root".equals(domain)) { + return ROOT_DIR; + } else if ("sharedpref".equals(domain)) { + return SHAREDPREF_DIR; + } else if ("external".equals(domain)) { + return EXTERNAL_DIR; + } else { + return null; + } + } + + /** + * Let's be strict about the type of xml the client can write. If we see anything untoward, + * throw an XmlPullParserException. + */ + private void validateInnerTagContents(XmlPullParser parser) + throws XmlPullParserException { + if (parser.getAttributeCount() > 2) { + throw new XmlPullParserException("At most 2 tag attributes allowed for \"" + + parser.getName() + "\" tag (\"domain\" & \"path\"."); + } + if (!"include".equals(parser.getName()) && !"exclude".equals(parser.getName())) { + throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" + + " \"<exclude/>. You provided \"" + parser.getName() + "\""); + } + } + } } diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 00248cc..1205708 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -1053,6 +1053,20 @@ public class AppWidgetManager { } } + /** + * @hide + */ + public boolean isBoundWidgetPackage(String packageName, int userId) { + if (mService == null) { + return false; + } + try { + return mService.isBoundWidgetPackage(packageName, userId); + } catch (RemoteException re) { + throw new RuntimeException("system server dead?", re); + } + } + private boolean bindAppWidgetIdIfAllowed(int appWidgetId, int profileId, ComponentName provider, Bundle options) { if (mService == null) { diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 6c32873..707ef30 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -96,6 +96,21 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public String backupAgentName; /** + * An optional attribute that indicates the app supports automatic backup of app data. + * <p>0 is the default and means the app's entire data folder + managed external storage will + * be backed up; + * Any negative value indicates the app does not support full-data backup, though it may still + * want to participate via the traditional key/value backup API; + * A positive number specifies an xml resource in which the application has defined its backup + * include/exclude criteria. + * <p>If android:allowBackup is set to false, this attribute is ignored. + * + * @see {@link android.content.Context#getNoBackupFilesDir} + * @see {@link #FLAG_ALLOW_BACKUP} + */ + public int fullBackupContent = 0; + + /** * The default extra UI options for activities in this application. * Set from the {@link android.R.attr#uiOptions} attribute in the * activity's manifest. @@ -686,6 +701,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { pw.println(prefix + "uiOptions=0x" + Integer.toHexString(uiOptions)); } pw.println(prefix + "supportsRtl=" + (hasRtlSupport() ? "true" : "false")); + if (fullBackupContent > 0) { + pw.println(prefix + "fullBackupContent=@xml/" + fullBackupContent); + } else { + pw.println(prefix + "fullBackupContent=" + (fullBackupContent < 0 ? "false" : "true")); + } super.dumpBack(pw, prefix); } @@ -763,6 +783,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uiOptions = orig.uiOptions; backupAgentName = orig.backupAgentName; hardwareAccelerated = orig.hardwareAccelerated; + fullBackupContent = orig.fullBackupContent; } @@ -816,6 +837,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(descriptionRes); dest.writeInt(uiOptions); dest.writeInt(hardwareAccelerated ? 1 : 0); + dest.writeInt(fullBackupContent); } public static final Parcelable.Creator<ApplicationInfo> CREATOR @@ -868,6 +890,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { descriptionRes = source.readInt(); uiOptions = source.readInt(); hardwareAccelerated = source.readInt() != 0; + fullBackupContent = source.readInt(); } /** diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.java b/core/java/android/content/pm/IntentFilterVerificationInfo.java index e50b0ff..96000dd 100644 --- a/core/java/android/content/pm/IntentFilterVerificationInfo.java +++ b/core/java/android/content/pm/IntentFilterVerificationInfo.java @@ -48,19 +48,18 @@ public final class IntentFilterVerificationInfo implements Parcelable { private static final String ATTR_PACKAGE_NAME = "packageName"; private static final String ATTR_STATUS = "status"; - private ArrayList<String> mDomains; + private ArraySet<String> mDomains = new ArraySet<>(); private String mPackageName; private int mMainStatus; public IntentFilterVerificationInfo() { mPackageName = null; - mDomains = new ArrayList<>(); mMainStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; } public IntentFilterVerificationInfo(String packageName, ArrayList<String> domains) { mPackageName = packageName; - mDomains = domains; + mDomains.addAll(domains); mMainStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; } @@ -73,14 +72,6 @@ public final class IntentFilterVerificationInfo implements Parcelable { readFromParcel(source); } - public ArrayList<String> getDomains() { - return mDomains; - } - - public ArraySet<String> getDomainsSet() { - return new ArraySet<>(mDomains); - } - public String getPackageName() { return mPackageName; } @@ -98,6 +89,14 @@ public final class IntentFilterVerificationInfo implements Parcelable { } } + public ArraySet<String> getDomains() { + return mDomains; + } + + public void setDomains(ArrayList<String> list) { + mDomains = new ArraySet<>(list); + } + public String getDomainsString() { StringBuilder sb = new StringBuilder(); for (String str : mDomains) { @@ -145,7 +144,6 @@ public final class IntentFilterVerificationInfo implements Parcelable { } mMainStatus = status; - mDomains = new ArrayList<>(); int outerDepth = parser.getDepth(); int type; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT @@ -201,15 +199,16 @@ public final class IntentFilterVerificationInfo implements Parcelable { private void readFromParcel(Parcel source) { mPackageName = source.readString(); mMainStatus = source.readInt(); - mDomains = new ArrayList<>(); - source.readStringList(mDomains); + ArrayList<String> list = new ArrayList<>(); + source.readStringList(list); + mDomains.addAll(list); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mPackageName); dest.writeInt(mMainStatus); - dest.writeStringList(mDomains); + dest.writeStringList(new ArrayList<>(mDomains)); } public static final Creator<IntentFilterVerificationInfo> CREATOR = @@ -221,5 +220,4 @@ public final class IntentFilterVerificationInfo implements Parcelable { return new IntentFilterVerificationInfo[size]; } }; - } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 9596c42..acc27c3 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2421,8 +2421,8 @@ public class PackageParser { if (allowBackup) { ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; - // backupAgent, killAfterRestore, and restoreAnyVersion are only relevant - // if backup is possible for the given application. + // backupAgent, killAfterRestore, fullBackupContent and restoreAnyVersion are only + // relevant if backup is possible for the given application. String backupAgent = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_backupAgent, Configuration.NATIVE_CONFIG_VERSION); @@ -2449,6 +2449,20 @@ public class PackageParser { ai.flags |= ApplicationInfo.FLAG_FULL_BACKUP_ONLY; } } + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestApplication_fullBackupContent); + if (v != null && (ai.fullBackupContent = v.resourceId) == 0) { + if (DEBUG_BACKUP) { + Slog.v(TAG, "fullBackupContent specified as boolean=" + + (v.data == 0 ? "false" : "true")); + } + // "false" => -1, "true" => 0 + ai.fullBackupContent = (v.data == 0 ? -1 : 0); + } + if (DEBUG_BACKUP) { + Slog.v(TAG, "fullBackupContent=" + ai.fullBackupContent + " for " + pkgName); + } } TypedValue v = sa.peekValue( diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index d88594d..1fc69c0 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -460,9 +460,8 @@ public class Camera { mEventHandler = null; } - String packageName = ActivityThread.currentPackageName(); - - return native_setup(new WeakReference<Camera>(this), cameraId, halVersion, packageName); + return native_setup(new WeakReference<Camera>(this), cameraId, halVersion, + ActivityThread.currentOpPackageName()); } private int cameraInitNormal(int cameraId) { diff --git a/core/java/android/hardware/ICameraService.aidl b/core/java/android/hardware/ICameraService.aidl index 9bc2f46..7b96e20 100644 --- a/core/java/android/hardware/ICameraService.aidl +++ b/core/java/android/hardware/ICameraService.aidl @@ -38,13 +38,13 @@ interface ICameraService int getCameraInfo(int cameraId, out CameraInfo info); int connect(ICameraClient client, int cameraId, - String clientPackageName, + String opPackageName, int clientUid, // Container for an ICamera object out BinderHolder device); int connectDevice(ICameraDeviceCallbacks callbacks, int cameraId, - String clientPackageName, + String opPackageName, int clientUid, // Container for an ICameraDeviceUser object out BinderHolder device); @@ -69,7 +69,7 @@ interface ICameraService int connectLegacy(ICameraClient client, int cameraId, int halVersion, - String clientPackageName, + String opPackageName, int clientUid, // Container for an ICamera object out BinderHolder device); diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 11037fd..22a9e9c 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -41,16 +41,19 @@ import java.util.List; */ public class SystemSensorManager extends SensorManager { private static native void nativeClassInit(); - private static native int nativeGetNextSensor(Sensor sensor, int next); - private static native int nativeEnableDataInjection(boolean enable); + private static native long nativeCreate(String opPackageName); + private static native int nativeGetNextSensor(long nativeInstance, Sensor sensor, int next); + private static native int nativeEnableDataInjection(long nativeInstance, boolean enable); private static boolean sSensorModuleInitialized = false; - private static final Object sSensorModuleLock = new Object(); - private static final ArrayList<Sensor> sFullSensorsList = new ArrayList<Sensor>(); - private static final SparseArray<Sensor> sHandleToSensor = new SparseArray<Sensor>(); private static InjectEventQueue mInjectEventQueue = null; private static boolean mDataInjectionMode = false; + private final Object mLock = new Object(); + + private final ArrayList<Sensor> mFullSensorsList = new ArrayList<>(); + private final SparseArray<Sensor> mHandleToSensor = new SparseArray<>(); + // Listener list private final HashMap<SensorEventListener, SensorEventQueue> mSensorListeners = new HashMap<SensorEventListener, SensorEventQueue>(); @@ -60,44 +63,44 @@ public class SystemSensorManager extends SensorManager { // Looper associated with the context in which this instance was created. private final Looper mMainLooper; private final int mTargetSdkLevel; - private final String mPackageName; + private final Context mContext; private final boolean mHasDataInjectionPermissions; + private final long mNativeInstance; /** {@hide} */ public SystemSensorManager(Context context, Looper mainLooper) { mMainLooper = mainLooper; mTargetSdkLevel = context.getApplicationInfo().targetSdkVersion; - mPackageName = context.getPackageName(); - synchronized(sSensorModuleLock) { + mContext = context; + mNativeInstance = nativeCreate(context.getOpPackageName()); + + synchronized(mLock) { if (!sSensorModuleInitialized) { sSensorModuleInitialized = true; - nativeClassInit(); - - // initialize the sensor list - final ArrayList<Sensor> fullList = sFullSensorsList; - int i = 0; - do { - Sensor sensor = new Sensor(); - i = nativeGetNextSensor(sensor, i); - if (i>=0) { - //Log.d(TAG, "found sensor: " + sensor.getName() + - // ", handle=" + sensor.getHandle()); - fullList.add(sensor); - sHandleToSensor.append(sensor.getHandle(), sensor); - } - } while (i>0); } mHasDataInjectionPermissions = context.checkSelfPermission( Manifest.permission.HARDWARE_TEST) == PackageManager.PERMISSION_GRANTED; } + + // initialize the sensor list + int i = 0; + while(true) { + Sensor sensor = new Sensor(); + i = nativeGetNextSensor(mNativeInstance, sensor, i); + if (i <= 0) { + break; + } + mFullSensorsList.add(sensor); + mHandleToSensor.append(sensor.getHandle(), sensor); + } } /** @hide */ @Override protected List<Sensor> getFullSensorList() { - return sFullSensorsList; + return mFullSensorsList; } @@ -232,8 +235,8 @@ public class SystemSensorManager extends SensorManager { throw new SecurityException("Permission denial. Calling enableDataInjection without " + Manifest.permission.HARDWARE_TEST); } - synchronized (sSensorModuleLock) { - int ret = nativeEnableDataInjection(enable); + synchronized (mLock) { + int ret = nativeEnableDataInjection(mNativeInstance, enable); // The HAL does not support injection. Ignore. if (ret != 0) { Log.e(TAG, "HAL does not support data injection"); @@ -255,7 +258,7 @@ public class SystemSensorManager extends SensorManager { throw new SecurityException("Permission denial. Calling injectSensorData without " + Manifest.permission.HARDWARE_TEST); } - synchronized (sSensorModuleLock) { + synchronized (mLock) { if (!mDataInjectionMode) { Log.e(TAG, "Data injection mode not activated before calling injectSensorData"); return false; @@ -284,15 +287,17 @@ public class SystemSensorManager extends SensorManager { * SensorManager instance. */ private static abstract class BaseEventQueue { - private native long nativeInitBaseEventQueue(WeakReference<BaseEventQueue> eventQWeak, - MessageQueue msgQ, float[] scratch, String packageName, int mode); + private static native long nativeInitBaseEventQueue(long nativeManager, + WeakReference<BaseEventQueue> eventQWeak, MessageQueue msgQ, float[] scratch, + String packageName, int mode, String opPackageName); private static native int nativeEnableSensor(long eventQ, int handle, int rateUs, int maxBatchReportLatencyUs); private static native int nativeDisableSensor(long eventQ, int handle); private static native void nativeDestroySensorEventQueue(long eventQ); private static native int nativeFlushSensor(long eventQ); private static native int nativeInjectSensorData(long eventQ, int handle, - float[] values,int accuracy, long timestamp); + float[] values,int accuracy, long timestamp); + private long nSensorEventQueue; private final SparseBooleanArray mActiveSensors = new SparseBooleanArray(); protected final SparseIntArray mSensorAccuracies = new SparseIntArray(); @@ -305,8 +310,9 @@ public class SystemSensorManager extends SensorManager { protected static final int OPERATING_MODE_DATA_INJECTION = 1; BaseEventQueue(Looper looper, SystemSensorManager manager, int mode) { - nSensorEventQueue = nativeInitBaseEventQueue(new WeakReference<BaseEventQueue>(this), - looper.getQueue(), mScratch, manager.mPackageName, mode); + nSensorEventQueue = nativeInitBaseEventQueue(manager.mNativeInstance, + new WeakReference<>(this), looper.getQueue(), mScratch, + manager.mContext.getPackageName(), mode, manager.mContext.getOpPackageName()); mCloseGuard.open("dispose"); mManager = manager; } @@ -339,7 +345,7 @@ public class SystemSensorManager extends SensorManager { for (int i=0 ; i<mActiveSensors.size(); i++) { if (mActiveSensors.valueAt(i) == true) { int handle = mActiveSensors.keyAt(i); - Sensor sensor = sHandleToSensor.get(handle); + Sensor sensor = mManager.mHandleToSensor.get(handle); if (sensor != null) { disableSensor(sensor); mActiveSensors.put(handle, false); @@ -452,7 +458,7 @@ public class SystemSensorManager extends SensorManager { @Override protected void dispatchSensorEvent(int handle, float[] values, int inAccuracy, long timestamp) { - final Sensor sensor = sHandleToSensor.get(handle); + final Sensor sensor = mManager.mHandleToSensor.get(handle); SensorEvent t = null; synchronized (mSensorsEvents) { t = mSensorsEvents.get(handle); @@ -481,7 +487,7 @@ public class SystemSensorManager extends SensorManager { @SuppressWarnings("unused") protected void dispatchFlushCompleteEvent(int handle) { if (mListener instanceof SensorEventListener2) { - final Sensor sensor = sHandleToSensor.get(handle); + final Sensor sensor = mManager.mHandleToSensor.get(handle); ((SensorEventListener2)mListener).onFlushCompleted(sensor); } return; @@ -519,7 +525,7 @@ public class SystemSensorManager extends SensorManager { @Override protected void dispatchSensorEvent(int handle, float[] values, int accuracy, long timestamp) { - final Sensor sensor = sHandleToSensor.get(handle); + final Sensor sensor = mManager.mHandleToSensor.get(handle); TriggerEvent t = null; synchronized (mTriggerEvents) { t = mTriggerEvents.get(handle); @@ -546,7 +552,7 @@ public class SystemSensorManager extends SensorManager { } } - static final class InjectEventQueue extends BaseEventQueue { + final class InjectEventQueue extends BaseEventQueue { public InjectEventQueue(Looper looper, SystemSensorManager manager) { super(looper, manager, OPERATING_MODE_DATA_INJECTION); } diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index f6791a4..e9564b3 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -580,7 +580,8 @@ public abstract class CameraDevice implements AutoCloseable { * indicating that the camera device is in use already. * * <p> - * This error can be produced when opening the camera fails. + * This error can be produced when opening the camera fails due to the camera + * being used by a higher-priority camera API client. * </p> * * @see #onError @@ -678,7 +679,7 @@ public abstract class CameraDevice implements AutoCloseable { * {@link CameraAccessException}. The disconnection could be due to a * change in security policy or permissions; the physical disconnection * of a removable camera device; or the camera being needed for a - * higher-priority use case.</p> + * higher-priority camera API client.</p> * * <p>There may still be capture callbacks that are invoked * after this method is called, or new image buffers that are delivered @@ -688,8 +689,9 @@ public abstract class CameraDevice implements AutoCloseable { * about the disconnection.</p> * * <p>You should clean up the camera with {@link CameraDevice#close} after - * this happens, as it is not recoverable until opening the camera again - * after it becomes {@link CameraManager.AvailabilityCallback#onCameraAvailable available}. + * this happens, as it is not recoverable until the camera can be opened + * again. For most use cases, this will be when the camera again becomes + * {@link CameraManager.AvailabilityCallback#onCameraAvailable available}. * </p> * * @param camera the device that has been disconnected diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 1a00a05..9327f00 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -77,8 +77,8 @@ public final class CameraManager { } /** - * Return the list of currently connected camera devices by - * identifier. + * Return the list of currently connected camera devices by identifier, including + * cameras that may be in use by other camera API clients. * * <p>Non-removable cameras use integers starting at 0 for their * identifiers, while removable cameras have a unique identifier for each @@ -103,6 +103,11 @@ public final class CameraManager { * <p>The first time a callback is registered, it is immediately called * with the availability status of all currently known camera devices.</p> * + * <p>{@link AvailabilityCallback#onCameraUnavailable(String)} will be called whenever a camera + * device is opened by any camera API client. As of API level 23, other camera API clients may + * still be able to open such a camera device, evicting the existing client if they have higher + * priority than the existing client of a camera device. See open() for more details.</p> + * * <p>Since this callback will be registered with the camera service, remember to unregister it * once it is no longer needed; otherwise the callback will continue to receive events * indefinitely and it may prevent other resources from being released. Specifically, the @@ -259,14 +264,14 @@ public final class CameraManager { } /** - * Helper for openning a connection to a camera with the given ID. + * Helper for opening a connection to a camera with the given ID. * * @param cameraId The unique identifier of the camera device to open * @param callback The callback for the camera. Must not be null. * @param handler The handler to invoke the callback on. Must not be null. * * @throws CameraAccessException if the camera is disabled by device policy, - * or too many camera devices are already open, or the cameraId does not match + * too many camera devices are already open, or the cameraId does not match * any currently available camera device. * * @throws SecurityException if the application does not have permission to @@ -309,7 +314,7 @@ public final class CameraManager { "Camera service is currently unavailable"); } cameraService.connectDevice(callbacks, id, - mContext.getPackageName(), USE_CALLING_UID, holder); + mContext.getOpPackageName(), USE_CALLING_UID, holder); cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder()); } else { // Use legacy camera implementation for HAL1 devices @@ -330,7 +335,8 @@ public final class CameraManager { deviceImpl.setRemoteFailure(e); if (e.getReason() == CameraAccessException.CAMERA_DISABLED || - e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) { + e.getReason() == CameraAccessException.CAMERA_DISCONNECTED || + e.getReason() == CameraAccessException.CAMERA_IN_USE) { // Per API docs, these failures call onError and throw throw e.asChecked(); } @@ -369,7 +375,19 @@ public final class CameraManager { * <p>Use {@link #getCameraIdList} to get the list of available camera * devices. Note that even if an id is listed, open may fail if the device * is disconnected between the calls to {@link #getCameraIdList} and - * {@link #openCamera}.</p> + * {@link #openCamera}, or if a higher-priority camera API client begins using the + * camera device.</p> + * + * <p>As of API level 23, devices for which the + * {@link AvailabilityCallback#onCameraUnavailable(String)} callback has been called due to the + * device being in use by a lower-priority, background camera API client can still potentially + * be opened by calling this method when the calling camera API client has a higher priority + * than the current camera API client using this device. In general, if the top, foreground + * activity is running within your application process, your process will be given the highest + * priority when accessing the camera, and this method will succeed even if the camera device is + * in use by another camera API client. Any lower-priority application that loses control of the + * camera in this way will receive an + * {@link android.hardware.camera2.CameraDevice.StateCallback#onDisconnected} callback.</p> * * <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up @@ -401,7 +419,7 @@ public final class CameraManager { * {@code null} to use the current thread's {@link android.os.Looper looper}. * * @throws CameraAccessException if the camera is disabled by device policy, - * or the camera has become or was disconnected. + * has been disconnected, or is being used by a higher-priority camera API client. * * @throws IllegalArgumentException if cameraId or the callback was null, * or the cameraId does not match any currently or previously available @@ -477,8 +495,7 @@ public final class CameraManager { } /** - * A callback for camera devices becoming available or - * unavailable to open. + * A callback for camera devices becoming available or unavailable to open. * * <p>Cameras become available when they are no longer in use, or when a new * removable camera is connected. They become unavailable when some diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index e3e16eb..8e0584a 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -20,6 +20,7 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; +import android.text.TextUtils; import android.util.Log; import java.io.File; @@ -242,6 +243,15 @@ public class Environment { return DATA_DIRECTORY; } + /** {@hide} */ + public static File getDataAppDirectory(String volumeUuid) { + if (TextUtils.isEmpty(volumeUuid)) { + return new File("/data/app"); + } else { + return new File("/mnt/expand/" + volumeUuid + "/app"); + } + } + /** * Return the primary external storage directory. This directory may not * currently be accessible if it has been mounted by the user on their diff --git a/core/java/android/os/IPermissionController.aidl b/core/java/android/os/IPermissionController.aidl index 0cc1603..5e8590a 100644 --- a/core/java/android/os/IPermissionController.aidl +++ b/core/java/android/os/IPermissionController.aidl @@ -21,4 +21,5 @@ package android.os; interface IPermissionController { boolean checkPermission(String permission, int pid, int uid); String[] getPackagesForUid(int uid); + boolean isRuntimePermission(String permission); } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 8c1f44f..1273772 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -502,7 +502,25 @@ public final class Parcel { * {@SystemApi} */ public final void writeBlob(byte[] b) { - nativeWriteBlob(mNativePtr, b, 0, (b != null) ? b.length : 0); + writeBlob(b, 0, (b != null) ? b.length : 0); + } + + /** + * Write a blob of data into the parcel at the current {@link #dataPosition}, + * growing {@link #dataCapacity} if needed. + * @param b Bytes to place into the parcel. + * @param offset Index of first byte to be written. + * @param len Number of bytes to write. + * {@hide} + * {@SystemApi} + */ + public final void writeBlob(byte[] b, int offset, int len) { + if (b == null) { + writeInt(-1); + return; + } + Arrays.checkOffsetAndCount(b.length, offset, len); + nativeWriteBlob(mNativePtr, b, offset, len); } /** diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java index 4bc97c9..997d586 100644 --- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java +++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java @@ -43,7 +43,7 @@ public class VoiceInteractionServiceInfo { private String mSessionService; private String mRecognitionService; private String mSettingsActivity; - private boolean mSupportsAssistGesture; + private boolean mSupportsAssist; public VoiceInteractionServiceInfo(PackageManager pm, ComponentName comp) throws PackageManager.NameNotFoundException { @@ -95,8 +95,8 @@ public class VoiceInteractionServiceInfo { com.android.internal.R.styleable.VoiceInteractionService_recognitionService); mSettingsActivity = array.getString( com.android.internal.R.styleable.VoiceInteractionService_settingsActivity); - mSupportsAssistGesture = array.getBoolean( - com.android.internal.R.styleable.VoiceInteractionService_supportsAssistGesture, + mSupportsAssist = array.getBoolean( + com.android.internal.R.styleable.VoiceInteractionService_supportsAssist, false); array.recycle(); if (mSessionService == null) { @@ -145,7 +145,7 @@ public class VoiceInteractionServiceInfo { return mSettingsActivity; } - public boolean getSupportsAssistGesture() { - return mSupportsAssistGesture; + public boolean getSupportsAssist() { + return mSupportsAssist; } } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 239b386..fc65f63 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -283,15 +283,14 @@ public class DynamicLayout extends Layout if (reflowed == null) { reflowed = new StaticLayout(null); - b = StaticLayout.Builder.obtain(text, where, where + after, getWidth()); + b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth()); } b.setText(text, where, where + after) .setPaint(getPaint()) .setWidth(getWidth()) .setTextDir(getTextDirectionHeuristic()) - .setSpacingMult(getSpacingMultiplier()) - .setSpacingAdd(getSpacingAdd()) + .setLineSpacing(getSpacingAdd(), getSpacingMultiplier()) .setEllipsizedWidth(mEllipsizedWidth) .setEllipsize(mEllipsizeAt) .setBreakStrategy(mBreakStrategy); diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 67794b1..451abea 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,6 +16,7 @@ package android.text; +import android.annotation.Nullable; import android.graphics.Paint; import android.text.style.LeadingMarginSpan; import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; @@ -46,18 +47,31 @@ public class StaticLayout extends Layout { static final String TAG = "StaticLayout"; /** - * Builder for static layouts. It would be better if this were a public - * API (as it would offer much greater flexibility for adding new options) - * but for the time being it's just internal. - * - * @hide + * Builder for static layouts. The builder is a newer pattern for constructing + * StaticLayout objects and should be preferred over the constructors, + * particularly to access newer features. To build a static layout, first + * call {@link #obtain} with the required arguments (text, paint, and width), + * then call setters for optional parameters, and finally {@link #build} + * to build the StaticLayout object. Parameters not explicitly set will get + * default values. */ public final static class Builder { private Builder() { mNativePtr = nNewBuilder(); } - public static Builder obtain(CharSequence source, int start, int end, int width) { + /** + * Obtain a builder for constructing StaticLayout objects + * + * @param source The text to be laid out, optionally with spans + * @param start The index of the start of the text + * @param end The index + 1 of the end of the text + * @param paint The base paint used for layout + * @param width The width in pixels + * @return a builder object used for constructing the StaticLayout + */ + public static Builder obtain(CharSequence source, int start, int end, TextPaint paint, + int width) { Builder b = sPool.acquire(); if (b == null) { b = new Builder(); @@ -67,6 +81,7 @@ public class StaticLayout extends Layout { b.mText = source; b.mStart = start; b.mEnd = end; + b.mPaint = paint; b.mWidth = width; b.mAlignment = Alignment.ALIGN_NORMAL; b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; @@ -98,6 +113,18 @@ public class StaticLayout extends Layout { return setText(source, 0, source.length()); } + /** + * Set the text. Only useful when re-using the builder, which is done for + * the internal implementation of {@link DynamicLayout} but not as part + * of normal {@link StaticLayout} usage. + * + * @param source The text to be laid out, optionally with spans + * @param start The index of the start of the text + * @param end The index + 1 of the end of the text + * @return this builder, useful for chaining + * + * @hide + */ public Builder setText(CharSequence source, int start, int end) { mText = source; mStart = start; @@ -105,11 +132,27 @@ public class StaticLayout extends Layout { return this; } + /** + * Set the paint. Internal for reuse cases only. + * + * @param paint The base paint used for layout + * @return this builder, useful for chaining + * + * @hide + */ public Builder setPaint(TextPaint paint) { mPaint = paint; return this; } + /** + * Set the width. Internal for reuse cases only. + * + * @param width The width in pixels + * @return this builder, useful for chaining + * + * @hide + */ public Builder setWidth(int width) { mWidth = width; if (mEllipsize == null) { @@ -118,53 +161,126 @@ public class StaticLayout extends Layout { return this; } + /** + * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}. + * + * @param alignment Alignment for the resulting {@link StaticLayout} + * @return this builder, useful for chaining + */ public Builder setAlignment(Alignment alignment) { mAlignment = alignment; return this; } + /** + * Set the text direction heuristic. The text direction heuristic is used to + * resolve text direction based per-paragraph based on the input text. The default is + * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. + * + * @param textDir text direction heuristic for resolving BiDi behavior. + * @return this builder, useful for chaining + */ public Builder setTextDir(TextDirectionHeuristic textDir) { mTextDir = textDir; return this; } - // TODO: combine the following, as they're almost always set together? - public Builder setSpacingMult(float spacingMult) { - mSpacingMult = spacingMult; - return this; - } - - public Builder setSpacingAdd(float spacingAdd) { + /** + * Set line spacing parameters. The default is 0.0 for {@code spacingAdd} + * and 1.0 for {@code spacingMult}. + * + * @param spacingAdd line spacing add + * @param spacingMult line spacing multiplier + * @return this builder, useful for chaining + * @see android.widget.TextView#setLineSpacing + */ + public Builder setLineSpacing(float spacingAdd, float spacingMult) { mSpacingAdd = spacingAdd; + mSpacingMult = spacingMult; return this; } + /** + * Set whether to include extra space beyond font ascent and descent (which is + * needed to avoid clipping in some languages, such as Arabic and Kannada). The + * default is {@code true}. + * + * @param includePad whether to include padding + * @return this builder, useful for chaining + * @see android.widget.TextView#setIncludeFontPadding + */ public Builder setIncludePad(boolean includePad) { mIncludePad = includePad; return this; } - // TODO: combine the following? + /** + * Set the width as used for ellipsizing purposes, if it differs from the + * normal layout width. The default is the {@code width} + * passed to {@link #obtain}. + * + * @param ellipsizedWidth width used for ellipsizing, in pixels + * @return this builder, useful for chaining + * @see android.widget.TextView#setEllipsize + */ public Builder setEllipsizedWidth(int ellipsizedWidth) { mEllipsizedWidth = ellipsizedWidth; return this; } - public Builder setEllipsize(TextUtils.TruncateAt ellipsize) { + /** + * Set ellipsizing on the layout. Causes words that are longer than the view + * is wide, or exceeding the number of lines (see #setMaxLines) in the case + * of {@link android.text.TextUtils.TruncateAt#END} or + * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead + * of broken. The default is + * {@code null}, indicating no ellipsis is to be applied. + * + * @param ellipsize type of ellipsis behavior + * @return this builder, useful for chaining + * @see android.widget.TextView#setEllipsize + */ + public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { mEllipsize = ellipsize; return this; } + /** + * Set maximum number of lines. This is particularly useful in the case of + * ellipsizing, where it changes the layout of the last line. The default is + * unlimited. + * + * @param maxLines maximum number of lines in the layout + * @return this builder, useful for chaining + * @see android.widget.TextView#setMaxLines + */ public Builder setMaxLines(int maxLines) { mMaxLines = maxLines; return this; } + /** + * Set break strategy, useful for selecting high quality or balanced paragraph + * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}. + * + * @param breakStrategy break strategy for paragraph layout + * @return this builder, useful for chaining + * @see android.widget.TextView#setBreakStrategy + */ public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { mBreakStrategy = breakStrategy; return this; } + /** + * Set indents. Arguments are arrays holding an indent amount, one per line, measured in + * pixels. For lines past the last element in the array, the last element repeats. + * + * @param leftIndents array of indent values for left margin, in pixels + * @param rightIndents array of indent values for right margin, in pixels + * @return this builder, useful for chaining + * @see android.widget.TextView#setIndents + */ public Builder setIndents(int[] leftIndents, int[] rightIndents) { int leftLen = leftIndents == null ? 0 : leftIndents.length; int rightLen = rightIndents == null ? 0 : rightIndents.length; @@ -218,6 +334,15 @@ public class StaticLayout extends Layout { nAddReplacementRun(mNativePtr, start, end, width); } + /** + * Build the {@link StaticLayout} after options have been set. + * + * <p>Note: the builder object must not be reused in any way after calling this + * method. Setting parameters after calling this method, or calling it a second + * time on the same builder object, will likely lead to unexpected results. + * + * @return the newly constructed {@link StaticLayout} object + */ public StaticLayout build() { StaticLayout result = new StaticLayout(this); Builder.recycle(this); @@ -327,12 +452,10 @@ public class StaticLayout extends Layout { : new Ellipsizer(source), paint, outerwidth, align, textDir, spacingmult, spacingadd); - Builder b = Builder.obtain(source, bufstart, bufend, outerwidth) - .setPaint(paint) + Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth) .setAlignment(align) .setTextDir(textDir) - .setSpacingMult(spacingmult) - .setSpacingAdd(spacingadd) + .setLineSpacing(spacingadd, spacingmult) .setIncludePad(includepad) .setEllipsizedWidth(ellipsizedWidth) .setEllipsize(ellipsize) diff --git a/core/java/android/view/GhostView.java b/core/java/android/view/GhostView.java index d58e7c0..bc38e1a 100644 --- a/core/java/android/view/GhostView.java +++ b/core/java/android/view/GhostView.java @@ -41,7 +41,7 @@ public class GhostView extends View { final ViewGroup parent = (ViewGroup) mView.getParent(); setGhostedVisibility(View.INVISIBLE); parent.mRecreateDisplayList = true; - parent.getDisplayList(); + parent.updateDisplayListIfDirty(); } @Override @@ -49,7 +49,7 @@ public class GhostView extends View { if (canvas instanceof DisplayListCanvas) { DisplayListCanvas dlCanvas = (DisplayListCanvas) canvas; mView.mRecreateDisplayList = true; - RenderNode renderNode = mView.getDisplayList(); + RenderNode renderNode = mView.updateDisplayListIfDirty(); if (renderNode.isValid()) { dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode dlCanvas.drawRenderNode(renderNode); @@ -84,7 +84,7 @@ public class GhostView extends View { final ViewGroup parent = (ViewGroup) mView.getParent(); if (parent != null) { parent.mRecreateDisplayList = true; - parent.getDisplayList(); + parent.updateDisplayListIfDirty(); } } } diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index 6db46e9..85b22fb 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -246,6 +246,9 @@ public class TextureView extends View { mSurface = null; mLayer = null; + // Make sure if/when new layer gets re-created, transform matrix will + // be re-applied. + mMatrixChanged = true; mHadSurface = true; } } diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index 25c5127..91e6d68 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -269,7 +269,7 @@ public class ThreadedRenderer extends HardwareRenderer { view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED; view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; - view.getDisplayList(); + view.updateDisplayListIfDirty(); view.mRecreateDisplayList = false; } @@ -285,7 +285,7 @@ public class ThreadedRenderer extends HardwareRenderer { callbacks.onHardwarePreDraw(canvas); canvas.insertReorderBarrier(); - canvas.drawRenderNode(view.getDisplayList()); + canvas.drawRenderNode(view.updateDisplayListIfDirty()); canvas.insertInorderBarrier(); callbacks.onHardwarePostDraw(canvas); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 81ad5ad..963b7a6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -11091,25 +11091,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * <p>Sets the opacity of the view. This is a value from 0 to 1, where 0 means the view is - * completely transparent and 1 means the view is completely opaque.</p> + * Sets the opacity of the view to a value from 0 to 1, where 0 means the view is + * completely transparent and 1 means the view is completely opaque. * - * <p> Note that setting alpha to a translucent value (0 < alpha < 1) can have significant - * performance implications, especially for large views. It is best to use the alpha property - * sparingly and transiently, as in the case of fading animations.</p> + * <p class="note"><strong>Note:</strong> setting alpha to a translucent value (0 < alpha < 1) + * can have significant performance implications, especially for large views. It is best to use + * the alpha property sparingly and transiently, as in the case of fading animations.</p> * * <p>For a view with a frequently changing alpha, such as during a fading animation, it is * strongly recommended for performance reasons to either override - * {@link #hasOverlappingRendering()} to return false if appropriate, or setting a - * {@link #setLayerType(int, android.graphics.Paint) layer type} on the view.</p> + * {@link #hasOverlappingRendering()} to return <code>false</code> if appropriate, or setting a + * {@link #setLayerType(int, android.graphics.Paint) layer type} on the view for the duration + * of the animation. On versions {@link android.os.Build.VERSION_CODES#MNC} and below, + * the default path for rendering an unlayered View with alpha could add multiple milliseconds + * of rendering cost, even for simple or small views. Starting with + * {@link android.os.Build.VERSION_CODES#MNC}, {@link #LAYER_TYPE_HARDWARE} is automatically + * applied to the view at the rendering level.</p> * * <p>If this view overrides {@link #onSetAlpha(int)} to return true, then this view is * responsible for applying the opacity itself.</p> * - * <p>Note that if the view is backed by a - * {@link #setLayerType(int, android.graphics.Paint) layer} and is associated with a - * {@link #setLayerPaint(android.graphics.Paint) layer paint}, setting an alpha value less than - * 1.0 will supersede the alpha of the layer paint.</p> + * <p>On versions {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and below, note that if + * the view is backed by a {@link #setLayerType(int, android.graphics.Paint) layer} and is + * associated with a {@link #setLayerPaint(android.graphics.Paint) layer paint}, setting an + * alpha value less than 1.0 will supersede the alpha of the layer paint.</p> + * + * <p>Starting with {@link android.os.Build.VERSION_CODES#MNC}, setting a translucent alpha + * value will clip a View to its bounds, unless the View returns <code>false</code> from + * {@link #hasOverlappingRendering}.</p> * * @param alpha The opacity of the view. * @@ -14702,11 +14711,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return !(mAttachInfo == null || mAttachInfo.mHardwareRenderer == null); } - private void updateDisplayListIfDirty() { + /** + * Gets the RenderNode for the view, and updates its DisplayList (if needed and supported) + * @hide + */ + @NonNull + public RenderNode updateDisplayListIfDirty() { final RenderNode renderNode = mRenderNode; if (!canHaveDisplayList()) { // can't populate RenderNode, don't try - return; + return renderNode; } if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 @@ -14720,7 +14734,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchGetDisplayList(); - return; // no work needed + return renderNode; // no work needed } // If we got here, we're recreating it. Mark it as such to ensure that @@ -14769,19 +14783,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; } - } - - /** - * Returns a RenderNode with View draw content recorded, which can be - * used to draw this view again without executing its draw method. - * - * @return A RenderNode ready to replay, or null if caching is not enabled. - * - * @hide - */ - public RenderNode getDisplayList() { - updateDisplayListIfDirty(); - return mRenderNode; + return renderNode; } private void resetDisplayList() { @@ -15543,7 +15545,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (drawingWithRenderNode) { // Delay getting the display list until animation-driven alpha values are // set up and possibly passed on to the view - renderNode = getDisplayList(); + renderNode = updateDisplayListIfDirty(); if (!renderNode.isValid()) { // Uncommon, but possible. If a view is removed from the hierarchy during the call // to getDisplayList(), the display list will be marked invalid and we should not diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index d0d4201..ef57dc3 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3524,10 +3524,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } private void recreateChildDisplayList(View child) { - child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) - == PFLAG_INVALIDATED; + child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0; child.mPrivateFlags &= ~PFLAG_INVALIDATED; - child.getDisplayList(); + child.updateDisplayListIfDirty(); child.mRecreateDisplayList = false; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4158340..fda6e63 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2322,10 +2322,8 @@ public final class ViewRootImpl implements ViewParent, * @hide */ void outputDisplayList(View view) { - RenderNode renderNode = view.getDisplayList(); - if (renderNode != null) { - renderNode.output(); - } + RenderNode renderNode = view.updateDisplayListIfDirty(); + renderNode.output(); } /** diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index c9d9a8c..1df43d0 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -3638,11 +3638,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te startNestedScroll(SCROLL_AXIS_VERTICAL); - if (mFastScroll != null) { - boolean intercepted = mFastScroll.onTouchEvent(ev); - if (intercepted) { - return true; - } + if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) { + return true; } initVelocityTrackerIfNotExists(); diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 35e7389..9b36b84 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -578,7 +578,12 @@ public class Editor { } private void hideCursorControllers() { - if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) { + // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost. + // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the + // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp() + // to distinguish one from the other. + if (mSuggestionsPopupWindow != null && ((mTextView instanceof ExtractEditText) || + !mSuggestionsPopupWindow.isShowingUp())) { // Should be done before hide insertion point controller since it triggers a show of it mSuggestionsPopupWindow.hide(); } @@ -1397,12 +1402,11 @@ public class Editor { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { if (imm.isActive(mTextView)) { - boolean reported = false; if (ims.mContentChanged || ims.mSelectionModeChanged) { // We are in extract mode and the content has changed // in some way... just report complete new text to the // input method. - reported = reportExtractedText(); + reportExtractedText(); } } } @@ -1919,10 +1923,6 @@ public class Editor { mSuggestionsPopupWindow.show(); } - boolean areSuggestionsShown() { - return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing(); - } - void onScrollChanged() { if (mPositionListener != null) { mPositionListener.onScrollChanged(); @@ -4620,8 +4620,6 @@ public class Editor { } static class InputMethodState { - Rect mCursorRectInWindow = new Rect(); - float[] mTmpOffset = new float[2]; ExtractedTextRequest mExtractedTextRequest; final ExtractedText mExtractedText = new ExtractedText(); int mBatchEditNesting; diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 552b274..f06f3c2 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -1389,7 +1389,8 @@ class FastScroller { // to intercept events. If it does, we will receive a CANCEL // event. if (!mList.isInScrollingContainer()) { - beginDrag(); + // This will get dispatched to onTouchEvent(). Start + // dragging there. return true; } @@ -1406,6 +1407,8 @@ class FastScroller { final float pos = getPosFromMotionEvent(mInitialTouchY); scrollTo(pos); + // This may get dispatched to onTouchEvent(), but it + // doesn't really matter since we'll already be in a drag. return onTouchEvent(ev); } break; @@ -1440,6 +1443,15 @@ class FastScroller { } switch (me.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + if (isPointInside(me.getX(), me.getY())) { + if (!mList.isInScrollingContainer()) { + beginDrag(); + return true; + } + } + } break; + case MotionEvent.ACTION_UP: { if (mPendingDrag >= 0) { // Allow a tap to scroll. diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 774a864..449173f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -6630,12 +6630,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // TODO: code duplication with makeSingleLayout() if (mHintLayout == null) { StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0, - mHint.length(), hintWidth) - .setPaint(mTextPaint) + mHint.length(), mTextPaint, hintWidth) .setAlignment(alignment) .setTextDir(mTextDir) - .setSpacingMult(mSpacingMult) - .setSpacingAdd(mSpacingAdd) + .setLineSpacing(mSpacingAdd, mSpacingMult) .setIncludePad(mIncludePad) .setBreakStrategy(mBreakStrategy); if (mLeftIndents != null || mRightIndents != null) { @@ -6721,12 +6719,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (result == null) { StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed, - 0, mTransformed.length(), wantWidth) - .setPaint(mTextPaint) + 0, mTransformed.length(), mTextPaint, wantWidth) .setAlignment(alignment) .setTextDir(mTextDir) - .setSpacingMult(mSpacingMult) - .setSpacingAdd(mSpacingAdd) + .setLineSpacing(mSpacingAdd, mSpacingMult) .setIncludePad(mIncludePad) .setBreakStrategy(mBreakStrategy); if (mLeftIndents != null || mRightIndents != null) { |
