[DO NOT MERGE] While-list apps to access account if already saw it am: 91d37f43c7
am: f56f70365d
Change-Id: I3a553a7efe34feb9fbfa5d5c504874a2780bf132
This commit is contained in:
@@ -63,6 +63,7 @@ LOCAL_SRC_FILES += \
|
|||||||
core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl \
|
core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl \
|
||||||
core/java/android/accounts/IAccountManager.aidl \
|
core/java/android/accounts/IAccountManager.aidl \
|
||||||
core/java/android/accounts/IAccountManagerResponse.aidl \
|
core/java/android/accounts/IAccountManagerResponse.aidl \
|
||||||
|
core/java/android/accounts/IAccountAccessTracker.aidl \
|
||||||
core/java/android/accounts/IAccountAuthenticator.aidl \
|
core/java/android/accounts/IAccountAuthenticator.aidl \
|
||||||
core/java/android/accounts/IAccountAuthenticatorResponse.aidl \
|
core/java/android/accounts/IAccountAuthenticatorResponse.aidl \
|
||||||
core/java/android/app/IActivityContainer.aidl \
|
core/java/android/app/IActivityContainer.aidl \
|
||||||
|
|||||||
@@ -16,9 +16,17 @@
|
|||||||
|
|
||||||
package android.accounts;
|
package android.accounts;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
|
import android.os.RemoteException;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.ArraySet;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.android.internal.annotations.GuardedBy;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value type that represents an Account in the {@link AccountManager}. This object is
|
* Value type that represents an Account in the {@link AccountManager}. This object is
|
||||||
@@ -26,8 +34,14 @@ import android.text.TextUtils;
|
|||||||
* suitable for use as the key of a {@link java.util.Map}
|
* suitable for use as the key of a {@link java.util.Map}
|
||||||
*/
|
*/
|
||||||
public class Account implements Parcelable {
|
public class Account implements Parcelable {
|
||||||
|
private static final String TAG = "Account";
|
||||||
|
|
||||||
|
@GuardedBy("sAccessedAccounts")
|
||||||
|
private static final Set<Account> sAccessedAccounts = new ArraySet<>();
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
public final String type;
|
public final String type;
|
||||||
|
private final @Nullable IAccountAccessTracker mAccessTracker;
|
||||||
|
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o == this) return true;
|
if (o == this) return true;
|
||||||
@@ -44,6 +58,20 @@ public class Account implements Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Account(String name, String type) {
|
public Account(String name, String type) {
|
||||||
|
this(name, type, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public Account(@NonNull Account other, @Nullable IAccountAccessTracker accessTracker) {
|
||||||
|
this(other.name, other.type, accessTracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public Account(String name, String type, IAccountAccessTracker accessTracker) {
|
||||||
if (TextUtils.isEmpty(name)) {
|
if (TextUtils.isEmpty(name)) {
|
||||||
throw new IllegalArgumentException("the name must not be empty: " + name);
|
throw new IllegalArgumentException("the name must not be empty: " + name);
|
||||||
}
|
}
|
||||||
@@ -52,11 +80,29 @@ public class Account implements Parcelable {
|
|||||||
}
|
}
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
this.mAccessTracker = accessTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Account(Parcel in) {
|
public Account(Parcel in) {
|
||||||
this.name = in.readString();
|
this.name = in.readString();
|
||||||
this.type = in.readString();
|
this.type = in.readString();
|
||||||
|
this.mAccessTracker = IAccountAccessTracker.Stub.asInterface(in.readStrongBinder());
|
||||||
|
if (mAccessTracker != null) {
|
||||||
|
synchronized (sAccessedAccounts) {
|
||||||
|
if (sAccessedAccounts.add(this)) {
|
||||||
|
try {
|
||||||
|
mAccessTracker.onAccountAccessed();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Error noting account access", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hide */
|
||||||
|
public IAccountAccessTracker getAccessTracker() {
|
||||||
|
return mAccessTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
@@ -66,6 +112,7 @@ public class Account implements Parcelable {
|
|||||||
public void writeToParcel(Parcel dest, int flags) {
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
dest.writeString(name);
|
dest.writeString(name);
|
||||||
dest.writeString(type);
|
dest.writeString(type);
|
||||||
|
dest.writeStrongInterface(mAccessTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<Account> CREATOR = new Creator<Account>() {
|
public static final Creator<Account> CREATOR = new Creator<Account>() {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package android.accounts;
|
|||||||
|
|
||||||
import static android.Manifest.permission.GET_ACCOUNTS;
|
import static android.Manifest.permission.GET_ACCOUNTS;
|
||||||
|
|
||||||
import android.annotation.IntRange;
|
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
import android.annotation.RequiresPermission;
|
import android.annotation.RequiresPermission;
|
||||||
import android.annotation.Size;
|
import android.annotation.Size;
|
||||||
@@ -179,6 +178,14 @@ public class AccountManager {
|
|||||||
*/
|
*/
|
||||||
public static final String KEY_ACCOUNT_TYPE = "accountType";
|
public static final String KEY_ACCOUNT_TYPE = "accountType";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle key used for the {@link IAccountAccessTracker} account access tracker
|
||||||
|
* used for noting the account was accessed when unmarshalled from a parcel.
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static final String KEY_ACCOUNT_ACCESS_TRACKER = "accountAccessTracker";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundle key used for the auth token value in results
|
* Bundle key used for the auth token value in results
|
||||||
* from {@link #getAuthToken} and friends.
|
* from {@link #getAuthToken} and friends.
|
||||||
@@ -268,13 +275,13 @@ public class AccountManager {
|
|||||||
public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
|
public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token for the special case where a UID has access only to an account
|
* Token type for the special case where a UID has access only to an account
|
||||||
* but no authenticator specific auth tokens.
|
* but no authenticator specific auth token types.
|
||||||
*
|
*
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
public static final String ACCOUNT_ACCESS_TOKEN =
|
public static final String ACCOUNT_ACCESS_TOKEN_TYPE =
|
||||||
"com.android.abbfd278-af8b-415d-af8b-7571d5dab133";
|
"com.android.AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE";
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final IAccountManager mService;
|
private final IAccountManager mService;
|
||||||
@@ -814,7 +821,9 @@ public class AccountManager {
|
|||||||
public Account bundleToResult(Bundle bundle) throws AuthenticatorException {
|
public Account bundleToResult(Bundle bundle) throws AuthenticatorException {
|
||||||
String name = bundle.getString(KEY_ACCOUNT_NAME);
|
String name = bundle.getString(KEY_ACCOUNT_NAME);
|
||||||
String type = bundle.getString(KEY_ACCOUNT_TYPE);
|
String type = bundle.getString(KEY_ACCOUNT_TYPE);
|
||||||
return new Account(name, type);
|
IAccountAccessTracker tracker = IAccountAccessTracker.Stub.asInterface(
|
||||||
|
bundle.getBinder(KEY_ACCOUNT_ACCESS_TRACKER));
|
||||||
|
return new Account(name, type, tracker);
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
@@ -2274,6 +2283,7 @@ public class AccountManager {
|
|||||||
result.putString(KEY_ACCOUNT_NAME, null);
|
result.putString(KEY_ACCOUNT_NAME, null);
|
||||||
result.putString(KEY_ACCOUNT_TYPE, null);
|
result.putString(KEY_ACCOUNT_TYPE, null);
|
||||||
result.putString(KEY_AUTHTOKEN, null);
|
result.putString(KEY_AUTHTOKEN, null);
|
||||||
|
result.putBinder(KEY_ACCOUNT_ACCESS_TRACKER, null);
|
||||||
try {
|
try {
|
||||||
mResponse.onResult(result);
|
mResponse.onResult(result);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
@@ -2299,9 +2309,13 @@ public class AccountManager {
|
|||||||
public void onResult(Bundle value) throws RemoteException {
|
public void onResult(Bundle value) throws RemoteException {
|
||||||
Account account = new Account(
|
Account account = new Account(
|
||||||
value.getString(KEY_ACCOUNT_NAME),
|
value.getString(KEY_ACCOUNT_NAME),
|
||||||
value.getString(KEY_ACCOUNT_TYPE));
|
value.getString(KEY_ACCOUNT_TYPE),
|
||||||
mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
|
IAccountAccessTracker.Stub.asInterface(
|
||||||
mActivity, mMyCallback, mHandler);
|
value.getBinder(
|
||||||
|
KEY_ACCOUNT_ACCESS_TRACKER)));
|
||||||
|
mFuture = getAuthToken(account, mAuthTokenType,
|
||||||
|
mLoginOptions, mActivity, mMyCallback,
|
||||||
|
mHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -2348,7 +2362,9 @@ public class AccountManager {
|
|||||||
setException(new AuthenticatorException("account not in result"));
|
setException(new AuthenticatorException("account not in result"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Account account = new Account(accountName, accountType);
|
final IAccountAccessTracker tracker = IAccountAccessTracker.Stub.asInterface(
|
||||||
|
result.getBinder(KEY_ACCOUNT_ACCESS_TRACKER));
|
||||||
|
final Account account = new Account(accountName, accountType, tracker);
|
||||||
mNumAccounts = 1;
|
mNumAccounts = 1;
|
||||||
getAuthToken(account, mAuthTokenType, null /* options */, mActivity,
|
getAuthToken(account, mAuthTokenType, null /* options */, mActivity,
|
||||||
mMyCallback, mHandler);
|
mMyCallback, mHandler);
|
||||||
|
|||||||
@@ -27,6 +27,21 @@ import android.os.RemoteCallback;
|
|||||||
*/
|
*/
|
||||||
public abstract class AccountManagerInternal {
|
public abstract class AccountManagerInternal {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for explicit UID account access grant changes.
|
||||||
|
*/
|
||||||
|
public interface OnAppPermissionChangeListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the explicit grant state for a given UID to
|
||||||
|
* access an account changes.
|
||||||
|
*
|
||||||
|
* @param account The account
|
||||||
|
* @param uid The UID for which the grant changed
|
||||||
|
*/
|
||||||
|
public void onAppPermissionChanged(Account account, int uid);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests that a given package is given access to an account.
|
* Requests that a given package is given access to an account.
|
||||||
* The provided callback will be invoked with a {@link android.os.Bundle}
|
* The provided callback will be invoked with a {@link android.os.Bundle}
|
||||||
@@ -38,7 +53,24 @@ public abstract class AccountManagerInternal {
|
|||||||
* @param userId Concrete user id for which to request.
|
* @param userId Concrete user id for which to request.
|
||||||
* @param callback A callback for receiving the result.
|
* @param callback A callback for receiving the result.
|
||||||
*/
|
*/
|
||||||
public abstract void requestAccountAccess(@NonNull Account account,
|
public abstract void requestAccountAccess(@NonNull Account account,
|
||||||
@NonNull String packageName, @IntRange(from = 0) int userId,
|
@NonNull String packageName, @IntRange(from = 0) int userId,
|
||||||
@NonNull RemoteCallback callback);
|
@NonNull RemoteCallback callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the given UID has access to the account.
|
||||||
|
*
|
||||||
|
* @param account The account
|
||||||
|
* @param uid The UID
|
||||||
|
* @return Whether the UID can access the account
|
||||||
|
*/
|
||||||
|
public abstract boolean hasAccountAccess(@NonNull Account account, @IntRange(from = 0) int uid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener for explicit UID account access grant changes.
|
||||||
|
*
|
||||||
|
* @param listener The listener
|
||||||
|
*/
|
||||||
|
public abstract void addOnAppPermissionChangeListener(
|
||||||
|
@NonNull OnAppPermissionChangeListener listener);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public class GrantCredentialsPermissionActivity extends Activity implements View
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!AccountManager.ACCOUNT_ACCESS_TOKEN.equals(mAuthTokenType)) {
|
if (!AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE.equals(mAuthTokenType)) {
|
||||||
AccountManager.get(this).getAuthTokenLabel(mAccount.type,
|
AccountManager.get(this).getAuthTokenLabel(mAccount.type,
|
||||||
mAuthTokenType, callback, null);
|
mAuthTokenType, callback, null);
|
||||||
}
|
}
|
||||||
|
|||||||
26
core/java/android/accounts/IAccountAccessTracker.aidl
Normal file
26
core/java/android/accounts/IAccountAccessTracker.aidl
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.accounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to track which apps accessed an account
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
oneway interface IAccountAccessTracker {
|
||||||
|
void onAccountAccessed();
|
||||||
|
}
|
||||||
@@ -1877,6 +1877,7 @@ public abstract class ContentResolver {
|
|||||||
if (extras != null) {
|
if (extras != null) {
|
||||||
String accountName = extras.getString(SYNC_EXTRAS_ACCOUNT);
|
String accountName = extras.getString(SYNC_EXTRAS_ACCOUNT);
|
||||||
if (!TextUtils.isEmpty(accountName)) {
|
if (!TextUtils.isEmpty(accountName)) {
|
||||||
|
// TODO: No references to Google in AOSP
|
||||||
account = new Account(accountName, "com.google");
|
account = new Account(accountName, "com.google");
|
||||||
}
|
}
|
||||||
extras.remove(SYNC_EXTRAS_ACCOUNT);
|
extras.remove(SYNC_EXTRAS_ACCOUNT);
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ import android.accounts.AccountManagerInternal;
|
|||||||
import android.accounts.AuthenticatorDescription;
|
import android.accounts.AuthenticatorDescription;
|
||||||
import android.accounts.CantAddAccountActivity;
|
import android.accounts.CantAddAccountActivity;
|
||||||
import android.accounts.GrantCredentialsPermissionActivity;
|
import android.accounts.GrantCredentialsPermissionActivity;
|
||||||
|
import android.accounts.IAccountAccessTracker;
|
||||||
import android.accounts.IAccountAuthenticator;
|
import android.accounts.IAccountAuthenticator;
|
||||||
import android.accounts.IAccountAuthenticatorResponse;
|
import android.accounts.IAccountAuthenticatorResponse;
|
||||||
import android.accounts.IAccountManager;
|
import android.accounts.IAccountManager;
|
||||||
import android.accounts.IAccountManagerResponse;
|
import android.accounts.IAccountManagerResponse;
|
||||||
import android.annotation.IntRange;
|
import android.annotation.IntRange;
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.app.ActivityManagerNative;
|
import android.app.ActivityManagerNative;
|
||||||
import android.app.ActivityThread;
|
import android.app.ActivityThread;
|
||||||
@@ -85,7 +87,6 @@ import android.os.SystemClock;
|
|||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.os.storage.StorageManager;
|
import android.os.storage.StorageManager;
|
||||||
import android.service.notification.StatusBarNotification;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
@@ -125,6 +126,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
@@ -283,7 +285,7 @@ public class AccountManagerService
|
|||||||
private final Object cacheLock = new Object();
|
private final Object cacheLock = new Object();
|
||||||
/** protected by the {@link #cacheLock} */
|
/** protected by the {@link #cacheLock} */
|
||||||
private final HashMap<String, Account[]> accountCache =
|
private final HashMap<String, Account[]> accountCache =
|
||||||
new LinkedHashMap<String, Account[]>();
|
new LinkedHashMap<>();
|
||||||
/** protected by the {@link #cacheLock} */
|
/** protected by the {@link #cacheLock} */
|
||||||
private final HashMap<Account, HashMap<String, String>> userDataCache =
|
private final HashMap<Account, HashMap<String, String>> userDataCache =
|
||||||
new HashMap<Account, HashMap<String, String>>();
|
new HashMap<Account, HashMap<String, String>>();
|
||||||
@@ -322,6 +324,8 @@ public class AccountManagerService
|
|||||||
|
|
||||||
private final SparseArray<UserAccounts> mUsers = new SparseArray<>();
|
private final SparseArray<UserAccounts> mUsers = new SparseArray<>();
|
||||||
private final SparseBooleanArray mLocalUnlockedUsers = new SparseBooleanArray();
|
private final SparseBooleanArray mLocalUnlockedUsers = new SparseBooleanArray();
|
||||||
|
private CopyOnWriteArrayList<AccountManagerInternal.OnAppPermissionChangeListener>
|
||||||
|
mAppPermissionChangeListeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private static AtomicReference<AccountManagerService> sThis = new AtomicReference<>();
|
private static AtomicReference<AccountManagerService> sThis = new AtomicReference<>();
|
||||||
private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{};
|
private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{};
|
||||||
@@ -502,7 +506,7 @@ public class AccountManagerService
|
|||||||
if (!checkAccess || hasAccountAccess(account, packageName,
|
if (!checkAccess || hasAccountAccess(account, packageName,
|
||||||
UserHandle.getUserHandleForUid(uid))) {
|
UserHandle.getUserHandleForUid(uid))) {
|
||||||
cancelNotification(getCredentialPermissionNotificationId(account,
|
cancelNotification(getCredentialPermissionNotificationId(account,
|
||||||
AccountManager.ACCOUNT_ACCESS_TOKEN, uid), packageName,
|
AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE, uid), packageName,
|
||||||
UserHandle.getUserHandleForUid(uid));
|
UserHandle.getUserHandleForUid(uid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -695,7 +699,8 @@ public class AccountManagerService
|
|||||||
final ArrayList<String> accountNames = cur.getValue();
|
final ArrayList<String> accountNames = cur.getValue();
|
||||||
final Account[] accountsForType = new Account[accountNames.size()];
|
final Account[] accountsForType = new Account[accountNames.size()];
|
||||||
for (int i = 0; i < accountsForType.length; i++) {
|
for (int i = 0; i < accountsForType.length; i++) {
|
||||||
accountsForType[i] = new Account(accountNames.get(i), accountType);
|
accountsForType[i] = new Account(accountNames.get(i), accountType,
|
||||||
|
new AccountAccessTracker());
|
||||||
}
|
}
|
||||||
accounts.accountCache.put(accountType, accountsForType);
|
accounts.accountCache.put(accountType, accountsForType);
|
||||||
}
|
}
|
||||||
@@ -1505,6 +1510,8 @@ public class AccountManagerService
|
|||||||
Bundle result = new Bundle();
|
Bundle result = new Bundle();
|
||||||
result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name);
|
result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name);
|
||||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type);
|
result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type);
|
||||||
|
result.putBinder(AccountManager.KEY_ACCOUNT_ACCESS_TRACKER,
|
||||||
|
resultingAccount.getAccessTracker().asBinder());
|
||||||
try {
|
try {
|
||||||
response.onResult(result);
|
response.onResult(result);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
@@ -1857,7 +1864,7 @@ public class AccountManagerService
|
|||||||
for (Pair<Pair<Account, String>, Integer> key
|
for (Pair<Pair<Account, String>, Integer> key
|
||||||
: accounts.credentialsPermissionNotificationIds.keySet()) {
|
: accounts.credentialsPermissionNotificationIds.keySet()) {
|
||||||
if (account.equals(key.first.first)
|
if (account.equals(key.first.first)
|
||||||
&& AccountManager.ACCOUNT_ACCESS_TOKEN.equals(key.first.second)) {
|
&& AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE.equals(key.first.second)) {
|
||||||
final int uid = (Integer) key.second;
|
final int uid = (Integer) key.second;
|
||||||
mMessageHandler.post(() -> cancelAccountAccessRequestNotificationIfNeeded(
|
mMessageHandler.post(() -> cancelAccountAccessRequestNotificationIfNeeded(
|
||||||
account, uid, false));
|
account, uid, false));
|
||||||
@@ -3457,22 +3464,36 @@ public class AccountManagerService
|
|||||||
Preconditions.checkArgumentInRange(userId, 0, Integer.MAX_VALUE, "user must be concrete");
|
Preconditions.checkArgumentInRange(userId, 0, Integer.MAX_VALUE, "user must be concrete");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
|
final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
|
||||||
// Use null token which means any token. Having a token means the package
|
return hasAccountAccess(account, packageName, uid);
|
||||||
// is trusted by the authenticator, hence it is fine to access the account.
|
|
||||||
if (permissionIsGranted(account, null, uid, userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// In addition to the permissions required to get an auth token we also allow
|
|
||||||
// the account to be accessed by holders of the get accounts permissions.
|
|
||||||
return checkUidPermission(Manifest.permission.GET_ACCOUNTS_PRIVILEGED, uid, packageName)
|
|
||||||
|| checkUidPermission(Manifest.permission.GET_ACCOUNTS, uid, packageName);
|
|
||||||
} catch (NameNotFoundException e) {
|
} catch (NameNotFoundException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasAccountAccess(@NonNull Account account, @Nullable String packageName,
|
||||||
|
int uid) {
|
||||||
|
if (packageName == null) {
|
||||||
|
String[] packageNames = mPackageManager.getPackagesForUid(uid);
|
||||||
|
if (ArrayUtils.isEmpty(packageNames)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// For app op checks related to permissions all packages in the UID
|
||||||
|
// have the same app op state, so doesn't matter which one we pick.
|
||||||
|
packageName = packageNames[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use null token which means any token. Having a token means the package
|
||||||
|
// is trusted by the authenticator, hence it is fine to access the account.
|
||||||
|
if (permissionIsGranted(account, null, uid, UserHandle.getUserId(uid))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// In addition to the permissions required to get an auth token we also allow
|
||||||
|
// the account to be accessed by holders of the get accounts permissions.
|
||||||
|
return checkUidPermission(Manifest.permission.GET_ACCOUNTS_PRIVILEGED, uid, packageName)
|
||||||
|
|| checkUidPermission(Manifest.permission.GET_ACCOUNTS, uid, packageName);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean checkUidPermission(String permission, int uid, String opPackageName) {
|
private boolean checkUidPermission(String permission, int uid, String opPackageName) {
|
||||||
final long identity = Binder.clearCallingIdentity();
|
final long identity = Binder.clearCallingIdentity();
|
||||||
try {
|
try {
|
||||||
@@ -3548,7 +3569,7 @@ public class AccountManagerService
|
|||||||
|
|
||||||
private void handleAuthenticatorResponse(boolean accessGranted) throws RemoteException {
|
private void handleAuthenticatorResponse(boolean accessGranted) throws RemoteException {
|
||||||
cancelNotification(getCredentialPermissionNotificationId(account,
|
cancelNotification(getCredentialPermissionNotificationId(account,
|
||||||
AccountManager.ACCOUNT_ACCESS_TOKEN, uid), packageName,
|
AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE, uid), packageName,
|
||||||
UserHandle.getUserHandleForUid(uid));
|
UserHandle.getUserHandleForUid(uid));
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
Bundle result = new Bundle();
|
Bundle result = new Bundle();
|
||||||
@@ -3556,7 +3577,7 @@ public class AccountManagerService
|
|||||||
callback.sendResult(result);
|
callback.sendResult(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), AccountManager.ACCOUNT_ACCESS_TOKEN, false);
|
}), AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -5610,6 +5631,12 @@ public class AccountManagerService
|
|||||||
|
|
||||||
cancelAccountAccessRequestNotificationIfNeeded(account, uid, true);
|
cancelAccountAccessRequestNotificationIfNeeded(account, uid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listeners are a final CopyOnWriteArrayList, hence no lock needed.
|
||||||
|
for (AccountManagerInternal.OnAppPermissionChangeListener listener
|
||||||
|
: mAppPermissionChangeListeners) {
|
||||||
|
mMessageHandler.post(() -> listener.onAppPermissionChanged(account, uid));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5642,9 +5669,16 @@ public class AccountManagerService
|
|||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid),
|
cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid),
|
||||||
new UserHandle(accounts.userId));
|
new UserHandle(accounts.userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listeners are a final CopyOnWriteArrayList, hence no lock needed.
|
||||||
|
for (AccountManagerInternal.OnAppPermissionChangeListener listener
|
||||||
|
: mAppPermissionChangeListeners) {
|
||||||
|
mMessageHandler.post(() -> listener.onAppPermissionChanged(account, uid));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final private String stringArrayToString(String[] value) {
|
static final private String stringArrayToString(String[] value) {
|
||||||
@@ -5683,7 +5717,7 @@ public class AccountManagerService
|
|||||||
if (accountsForType != null) {
|
if (accountsForType != null) {
|
||||||
System.arraycopy(accountsForType, 0, newAccountsForType, 0, oldLength);
|
System.arraycopy(accountsForType, 0, newAccountsForType, 0, oldLength);
|
||||||
}
|
}
|
||||||
newAccountsForType[oldLength] = account;
|
newAccountsForType[oldLength] = new Account(account, new AccountAccessTracker());
|
||||||
accounts.accountCache.put(account.type, newAccountsForType);
|
accounts.accountCache.put(account.type, newAccountsForType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5927,6 +5961,33 @@ public class AccountManagerService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class AccountAccessTracker extends IAccountAccessTracker.Stub {
|
||||||
|
@Override
|
||||||
|
public void onAccountAccessed() throws RemoteException {
|
||||||
|
final int uid = Binder.getCallingUid();
|
||||||
|
if (UserHandle.getAppId(uid) == Process.SYSTEM_UID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int userId = UserHandle.getCallingUserId();
|
||||||
|
final long identity = Binder.clearCallingIdentity();
|
||||||
|
try {
|
||||||
|
for (Account account : getAccounts(userId, mContext.getOpPackageName())) {
|
||||||
|
IAccountAccessTracker accountTracker = account.getAccessTracker();
|
||||||
|
if (accountTracker != null && asBinder() == accountTracker.asBinder()) {
|
||||||
|
// An app just accessed the account. At this point it knows about
|
||||||
|
// it and there is not need to hide this account from the app.
|
||||||
|
if (!hasAccountAccess(account, null, uid)) {
|
||||||
|
updateAppPermission(account, AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE,
|
||||||
|
uid, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Binder.restoreCallingIdentity(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class AccountManagerInternalImpl extends AccountManagerInternal {
|
private final class AccountManagerInternalImpl extends AccountManagerInternal {
|
||||||
@Override
|
@Override
|
||||||
public void requestAccountAccess(@NonNull Account account, @NonNull String packageName,
|
public void requestAccountAccess(@NonNull Account account, @NonNull String packageName,
|
||||||
@@ -5948,7 +6009,8 @@ public class AccountManagerService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAccountAccess(account, packageName, new UserHandle(userId))) {
|
if (AccountManagerService.this.hasAccountAccess(account, packageName,
|
||||||
|
new UserHandle(userId))) {
|
||||||
Bundle result = new Bundle();
|
Bundle result = new Bundle();
|
||||||
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
|
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
|
||||||
callback.sendResult(result);
|
callback.sendResult(result);
|
||||||
@@ -5964,7 +6026,22 @@ public class AccountManagerService
|
|||||||
}
|
}
|
||||||
|
|
||||||
Intent intent = newRequestAccountAccessIntent(account, packageName, uid, callback);
|
Intent intent = newRequestAccountAccessIntent(account, packageName, uid, callback);
|
||||||
doNotification(mUsers.get(userId), account, null, intent, packageName, userId);
|
final UserAccounts userAccounts;
|
||||||
|
synchronized (mUsers) {
|
||||||
|
userAccounts = mUsers.get(userId);
|
||||||
|
}
|
||||||
|
doNotification(userAccounts, account, null, intent, packageName, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addOnAppPermissionChangeListener(OnAppPermissionChangeListener listener) {
|
||||||
|
// Listeners are a final CopyOnWriteArrayList, hence no lock needed.
|
||||||
|
mAppPermissionChangeListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasAccountAccess(@NonNull Account account, @IntRange(from = 0) int uid) {
|
||||||
|
return AccountManagerService.this.hasAccountAccess(account, null, uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ public final class ContentService extends IContentService.Stub {
|
|||||||
SyncManager syncManager = getSyncManager();
|
SyncManager syncManager = getSyncManager();
|
||||||
if (syncManager != null) {
|
if (syncManager != null) {
|
||||||
syncManager.scheduleSync(account, userId, uId, authority, extras,
|
syncManager.scheduleSync(account, userId, uId, authority, extras,
|
||||||
false /* onlyThoseWithUnkownSyncableState */);
|
SyncStorageEngine.AuthorityInfo.UNDEFINED);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
restoreCallingIdentity(identityToken);
|
restoreCallingIdentity(identityToken);
|
||||||
@@ -548,7 +548,7 @@ public final class ContentService extends IContentService.Stub {
|
|||||||
} else {
|
} else {
|
||||||
syncManager.scheduleSync(
|
syncManager.scheduleSync(
|
||||||
request.getAccount(), userId, callerUid, request.getProvider(), extras,
|
request.getAccount(), userId, callerUid, request.getProvider(), extras,
|
||||||
false /* onlyThoseWithUnknownSyncableState */);
|
SyncStorageEngine.AuthorityInfo.UNDEFINED);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
restoreCallingIdentity(identityToken);
|
restoreCallingIdentity(identityToken);
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ public class SyncManager {
|
|||||||
@Override
|
@Override
|
||||||
public void onSyncRequest(SyncStorageEngine.EndPoint info, int reason, Bundle extras) {
|
public void onSyncRequest(SyncStorageEngine.EndPoint info, int reason, Bundle extras) {
|
||||||
scheduleSync(info.account, info.userId, reason, info.provider, extras,
|
scheduleSync(info.account, info.userId, reason, info.provider, extras,
|
||||||
false);
|
AuthorityInfo.UNDEFINED);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -534,7 +534,7 @@ public class SyncManager {
|
|||||||
if (!removed) {
|
if (!removed) {
|
||||||
scheduleSync(null, UserHandle.USER_ALL,
|
scheduleSync(null, UserHandle.USER_ALL,
|
||||||
SyncOperation.REASON_SERVICE_CHANGED,
|
SyncOperation.REASON_SERVICE_CHANGED,
|
||||||
type.authority, null, false /* onlyThoseWithUnkownSyncableState */);
|
type.authority, null, AuthorityInfo.UNDEFINED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, mSyncHandler);
|
}, mSyncHandler);
|
||||||
@@ -576,6 +576,15 @@ public class SyncManager {
|
|||||||
mAccountManager = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE);
|
mAccountManager = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE);
|
||||||
mAccountManagerInternal = LocalServices.getService(AccountManagerInternal.class);
|
mAccountManagerInternal = LocalServices.getService(AccountManagerInternal.class);
|
||||||
|
|
||||||
|
mAccountManagerInternal.addOnAppPermissionChangeListener((Account account, int uid) -> {
|
||||||
|
// If the UID gained access to the account kick-off syncs lacking account access
|
||||||
|
if (mAccountManagerInternal.hasAccountAccess(account, uid)) {
|
||||||
|
scheduleSync(account, UserHandle.getUserId(uid),
|
||||||
|
SyncOperation.REASON_ACCOUNTS_UPDATED,
|
||||||
|
null, null, AuthorityInfo.SYNCABLE_NO_ACCOUNT_ACCESS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService(
|
mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService(
|
||||||
BatteryStats.SERVICE_NAME));
|
BatteryStats.SERVICE_NAME));
|
||||||
|
|
||||||
@@ -671,7 +680,7 @@ public class SyncManager {
|
|||||||
service.type.accountType, userHandle)) {
|
service.type.accountType, userHandle)) {
|
||||||
if (!canAccessAccount(account, packageName, userId)) {
|
if (!canAccessAccount(account, packageName, userId)) {
|
||||||
mAccountManager.updateAppPermission(account,
|
mAccountManager.updateAppPermission(account,
|
||||||
AccountManager.ACCOUNT_ACCESS_TOKEN, service.uid, true);
|
AccountManager.ACCOUNT_ACCESS_TOKEN_TYPE, service.uid, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -778,10 +787,11 @@ public class SyncManager {
|
|||||||
* @param extras a Map of SyncAdapter-specific information to control
|
* @param extras a Map of SyncAdapter-specific information to control
|
||||||
* syncs of a specific provider. Can be null. Is ignored
|
* syncs of a specific provider. Can be null. Is ignored
|
||||||
* if the url is null.
|
* if the url is null.
|
||||||
* @param onlyThoseWithUnkownSyncableState Only sync authorities that have unknown state.
|
* @param targetSyncState Only sync authorities that have the specified sync state.
|
||||||
|
* Use {@link AuthorityInfo#UNDEFINED} to sync all authorities.
|
||||||
*/
|
*/
|
||||||
public void scheduleSync(Account requestedAccount, int userId, int reason,
|
public void scheduleSync(Account requestedAccount, int userId, int reason,
|
||||||
String requestedAuthority, Bundle extras, boolean onlyThoseWithUnkownSyncableState) {
|
String requestedAuthority, Bundle extras, int targetSyncState) {
|
||||||
final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
|
final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
|
||||||
if (extras == null) {
|
if (extras == null) {
|
||||||
extras = new Bundle();
|
extras = new Bundle();
|
||||||
@@ -888,7 +898,7 @@ public class SyncManager {
|
|||||||
if (result != null
|
if (result != null
|
||||||
&& result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) {
|
&& result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) {
|
||||||
scheduleSync(account.account, userId, reason, authority,
|
scheduleSync(account.account, userId, reason, authority,
|
||||||
finalExtras, onlyThoseWithUnkownSyncableState);
|
finalExtras, targetSyncState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -903,9 +913,10 @@ public class SyncManager {
|
|||||||
isSyncable = AuthorityInfo.SYNCABLE;
|
isSyncable = AuthorityInfo.SYNCABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) {
|
if (targetSyncState != AuthorityInfo.UNDEFINED && targetSyncState != isSyncable) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) {
|
if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -931,7 +942,7 @@ public class SyncManager {
|
|||||||
|
|
||||||
final String owningPackage = syncAdapterInfo.componentName.getPackageName();
|
final String owningPackage = syncAdapterInfo.componentName.getPackageName();
|
||||||
|
|
||||||
if (isSyncable < 0) {
|
if (isSyncable == AuthorityInfo.NOT_INITIALIZED) {
|
||||||
// Initialisation sync.
|
// Initialisation sync.
|
||||||
Bundle newExtras = new Bundle();
|
Bundle newExtras = new Bundle();
|
||||||
newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
|
newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
|
||||||
@@ -950,8 +961,8 @@ public class SyncManager {
|
|||||||
owningUid, owningPackage, reason, source,
|
owningUid, owningPackage, reason, source,
|
||||||
authority, newExtras, allowParallelSyncs)
|
authority, newExtras, allowParallelSyncs)
|
||||||
);
|
);
|
||||||
}
|
} else if (targetSyncState == AuthorityInfo.UNDEFINED
|
||||||
if (!onlyThoseWithUnkownSyncableState) {
|
|| targetSyncState == isSyncable) {
|
||||||
if (isLoggable) {
|
if (isLoggable) {
|
||||||
Slog.v(TAG, "scheduleSync:"
|
Slog.v(TAG, "scheduleSync:"
|
||||||
+ " delay until " + delayUntil
|
+ " delay until " + delayUntil
|
||||||
@@ -1076,7 +1087,7 @@ public class SyncManager {
|
|||||||
final Bundle extras = new Bundle();
|
final Bundle extras = new Bundle();
|
||||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
|
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
|
||||||
scheduleSync(account, userId, reason, authority, extras,
|
scheduleSync(account, userId, reason, authority, extras,
|
||||||
false /* onlyThoseWithUnkownSyncableState */);
|
AuthorityInfo.UNDEFINED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncAdapterType[] getSyncAdapterTypes(int userId) {
|
public SyncAdapterType[] getSyncAdapterTypes(int userId) {
|
||||||
@@ -1535,7 +1546,7 @@ public class SyncManager {
|
|||||||
mContext.getOpPackageName());
|
mContext.getOpPackageName());
|
||||||
for (Account account : accounts) {
|
for (Account account : accounts) {
|
||||||
scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null,
|
scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null,
|
||||||
true /* onlyThoseWithUnknownSyncableState */);
|
AuthorityInfo.NOT_INITIALIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2714,7 +2725,8 @@ public class SyncManager {
|
|||||||
|
|
||||||
if (syncTargets != null) {
|
if (syncTargets != null) {
|
||||||
scheduleSync(syncTargets.account, syncTargets.userId,
|
scheduleSync(syncTargets.account, syncTargets.userId,
|
||||||
SyncOperation.REASON_ACCOUNTS_UPDATED, syncTargets.provider, null, true);
|
SyncOperation.REASON_ACCOUNTS_UPDATED, syncTargets.provider,
|
||||||
|
null, AuthorityInfo.NOT_INITIALIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,12 @@ public class SyncStorageEngine extends Handler {
|
|||||||
|
|
||||||
public static class AuthorityInfo {
|
public static class AuthorityInfo {
|
||||||
// Legal values of getIsSyncable
|
// Legal values of getIsSyncable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The syncable state is undefined.
|
||||||
|
*/
|
||||||
|
public static final int UNDEFINED = -2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default state for a newly installed adapter. An uninitialized adapter will receive an
|
* Default state for a newly installed adapter. An uninitialized adapter will receive an
|
||||||
* initialization sync which are governed by a different set of rules to that of regular
|
* initialization sync which are governed by a different set of rules to that of regular
|
||||||
|
|||||||
Reference in New Issue
Block a user