diff --git a/api/current.xml b/api/current.xml index fcebc736af310..c6d68ef9d08a8 100644 --- a/api/current.xml +++ b/api/current.xml @@ -122,6 +122,28 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mSessions = new LinkedHashMap(); - private static final int NOTIFICATION_ID = 234; + private final AtomicInteger mNotificationIds = new AtomicInteger(1); + + private final HashMap, Integer>, Integer> + mCredentialsPermissionNotificationIds = + new HashMap, Integer>, Integer>(); + private final HashMap mSigninRequiredNotificationIds = + new HashMap(); + private static AtomicReference sThis = + new AtomicReference(); + + private static final boolean isDebuggableMonkeyBuild = + SystemProperties.getBoolean("ro.monkey", false) + && SystemProperties.getBoolean("ro.debuggable", false); + /** + * This should only be called by system code. One should only call this after the service + * has started. + * @return a reference to the AccountManagerService instance + * @hide + */ + public static AccountManagerService getSingleton() { + return sThis.get(); + } public class AuthTokenKey { public final Account mAccount; @@ -163,9 +206,12 @@ public class AccountManagerService extends IAccountManager.Stub { MESSAGE_CONNECTED, MESSAGE_DISCONNECTED); mSimWatcher = new SimWatcher(mContext); + sThis.set(this); } public String getPassword(Account account) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); try { SQLiteDatabase db = mOpenHelper.getReadableDatabase(); @@ -186,6 +232,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public String getUserData(Account account, String key) { + checkAuthenticateAccountsPermission(account); long identityToken = clearCallingIdentity(); try { SQLiteDatabase db = mOpenHelper.getReadableDatabase(); @@ -207,7 +254,6 @@ public class AccountManagerService extends IAccountManager.Stub { cursor.close(); } } finally { - db.setTransactionSuccessful(); db.endTransaction(); } } finally { @@ -235,6 +281,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public Account[] getAccounts() { + checkReadAccountsPermission(); long identityToken = clearCallingIdentity(); try { return getAccountsByType(null); @@ -244,6 +291,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public Account[] getAccountsByType(String accountType) { + checkReadAccountsPermission(); long identityToken = clearCallingIdentity(); try { SQLiteDatabase db = mOpenHelper.getReadableDatabase(); @@ -269,6 +317,8 @@ public class AccountManagerService extends IAccountManager.Stub { } public boolean addAccount(Account account, String password, Bundle extras) { + checkAuthenticateAccountsPermission(account); + // fails if the account already exists long identityToken = clearCallingIdentity(); try { @@ -318,6 +368,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void removeAccount(Account account) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); @@ -330,6 +381,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void invalidateAuthToken(String accountType, String authToken) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); @@ -404,12 +456,12 @@ public class AccountManagerService extends IAccountManager.Stub { } return getAuthToken(db, accountId, authTokenType); } finally { - db.setTransactionSuccessful(); db.endTransaction(); } } public String peekAuthToken(Account account, String authTokenType) { + checkAuthenticateAccountsPermission(account); long identityToken = clearCallingIdentity(); try { return readAuthTokenFromDatabase(account, authTokenType); @@ -419,6 +471,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void setAuthToken(Account account, String authTokenType, String authToken) { + checkAuthenticateAccountsPermission(account); long identityToken = clearCallingIdentity(); try { cacheAuthToken(account, authTokenType, authToken); @@ -428,6 +481,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void setPassword(Account account, String password) { + checkAuthenticateAccountsPermission(account); long identityToken = clearCallingIdentity(); try { ContentValues values = new ContentValues(); @@ -446,6 +500,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void clearPassword(Account account) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { setPassword(account, null); @@ -455,6 +510,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void setUserData(Account account, String key, String value) { + checkAuthenticateAccountsPermission(account); long identityToken = clearCallingIdentity(); try { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); @@ -487,26 +543,39 @@ public class AccountManagerService extends IAccountManager.Stub { } } + private void onResult(IAccountManagerResponse response, Bundle result) { + try { + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote + // exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + public void getAuthToken(IAccountManagerResponse response, final Account account, final String authTokenType, final boolean notifyOnAuthFailure, final boolean expectActivityLaunch, final Bundle loginOptions) { + checkBinderPermission(Manifest.permission.USE_CREDENTIALS); + final int callerUid = Binder.getCallingUid(); + final boolean permissionGranted = permissionIsGranted(account, authTokenType, callerUid); + long identityToken = clearCallingIdentity(); try { - String authToken = readAuthTokenFromDatabase(account, authTokenType); - if (authToken != null) { - try { + // if the caller has permission, do the peek. otherwise go the more expensive + // route of starting a Session + if (permissionGranted) { + String authToken = readAuthTokenFromDatabase(account, authTokenType); + if (authToken != null) { Bundle result = new Bundle(); result.putString(Constants.AUTHTOKEN_KEY, authToken); result.putString(Constants.ACCOUNT_NAME_KEY, account.mName); result.putString(Constants.ACCOUNT_TYPE_KEY, account.mType); - response.onResult(result); - } catch (RemoteException e) { - // if the caller is dead then there is no one to care about remote exceptions - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "failure while notifying response", e); - } + onResult(response, result); + return; } - return; } new Session(response, account.mType, expectActivityLaunch) { @@ -520,11 +589,27 @@ public class AccountManagerService extends IAccountManager.Stub { } public void run() throws RemoteException { - mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); + // If the caller doesn't have permission then create and return the + // "grant permission" intent instead of the "getAuthToken" intent. + if (!permissionGranted) { + mAuthenticator.getAuthTokenLabel(this, authTokenType); + } else { + mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); + } } public void onResult(Bundle result) { if (result != null) { + if (result.containsKey(Constants.AUTH_TOKEN_LABEL_KEY)) { + Intent intent = newGrantCredentialsPermissionIntent(account, callerUid, + new AccountAuthenticatorResponse(this), + authTokenType, + result.getString(Constants.AUTH_TOKEN_LABEL_KEY)); + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.INTENT_KEY, intent); + onResult(bundle); + return; + } String authToken = result.getString(Constants.AUTHTOKEN_KEY); if (authToken != null) { String name = result.getString(Constants.ACCOUNT_NAME_KEY); @@ -539,7 +624,8 @@ public class AccountManagerService extends IAccountManager.Stub { Intent intent = result.getParcelable(Constants.INTENT_KEY); if (intent != null && notifyOnAuthFailure) { - doNotification(result.getString(Constants.AUTH_FAILED_MESSAGE_KEY), + doNotification( + account, result.getString(Constants.AUTH_FAILED_MESSAGE_KEY), intent); } } @@ -551,10 +637,92 @@ public class AccountManagerService extends IAccountManager.Stub { } } + private void createNoCredentialsPermissionNotification(Account account, Intent intent) { + int uid = intent.getIntExtra( + GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, -1); + String authTokenType = intent.getStringExtra( + GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE); + String authTokenLabel = intent.getStringExtra( + GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL); + + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + final CharSequence subtitleFormatString = + mContext.getText(R.string.permission_request_notification_subtitle); + n.setLatestEventInfo(mContext, + mContext.getText(R.string.permission_request_notification_title), + String.format(subtitleFormatString.toString(), account.mName), + PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(getCredentialPermissionNotificationId(account, authTokenType, uid), n); + } + + private Intent newGrantCredentialsPermissionIntent(Account account, int uid, + AccountAuthenticatorResponse response, String authTokenType, String authTokenLabel) { + RegisteredServicesCache.ServiceInfo serviceInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(account.mType)); + if (serviceInfo == null) { + throw new IllegalArgumentException("unknown account type: " + account.mType); + } + + final Context authContext; + try { + authContext = mContext.createPackageContext( + serviceInfo.type.packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("unknown account type: " + account.mType); + } + + Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory( + String.valueOf(getCredentialPermissionNotificationId(account, authTokenType, uid))); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT, account); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL, authTokenLabel); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE, authTokenType); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_RESPONSE, response); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT_TYPE_LABEL, + authContext.getString(serviceInfo.type.labelId)); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_PACKAGES, + mContext.getPackageManager().getPackagesForUid(uid)); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, uid); + return intent; + } + + private Integer getCredentialPermissionNotificationId(Account account, String authTokenType, + int uid) { + Integer id; + synchronized(mCredentialsPermissionNotificationIds) { + final Pair, Integer> key = + new Pair, Integer>( + new Pair(account, authTokenType), uid); + id = mCredentialsPermissionNotificationIds.get(key); + if (id == null) { + id = mNotificationIds.incrementAndGet(); + mCredentialsPermissionNotificationIds.put(key, id); + } + } + return id; + } + + private Integer getSigninRequiredNotificationId(Account account) { + Integer id; + synchronized(mSigninRequiredNotificationIds) { + id = mSigninRequiredNotificationIds.get(account); + if (id == null) { + id = mNotificationIds.incrementAndGet(); + mSigninRequiredNotificationIds.put(account, id); + } + } + return id; + } + public void addAcount(final IAccountManagerResponse response, final String accountType, final String authTokenType, final String[] requiredFeatures, final boolean expectActivityLaunch, final Bundle options) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { new Session(response, accountType, expectActivityLaunch) { @@ -579,6 +747,7 @@ public class AccountManagerService extends IAccountManager.Stub { public void confirmCredentials(IAccountManagerResponse response, final Account account, final boolean expectActivityLaunch) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { new Session(response, account.mType, expectActivityLaunch) { @@ -597,6 +766,7 @@ public class AccountManagerService extends IAccountManager.Stub { public void confirmPassword(IAccountManagerResponse response, final Account account, final String password) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { new Session(response, account.mType, false /* expectActivityLaunch */) { @@ -616,6 +786,7 @@ public class AccountManagerService extends IAccountManager.Stub { public void updateCredentials(IAccountManagerResponse response, final Account account, final String authTokenType, final boolean expectActivityLaunch, final Bundle loginOptions) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { new Session(response, account.mType, expectActivityLaunch) { @@ -637,6 +808,7 @@ public class AccountManagerService extends IAccountManager.Stub { public void editProperties(IAccountManagerResponse response, final String accountType, final boolean expectActivityLaunch) { + checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { new Session(response, accountType, expectActivityLaunch) { @@ -728,6 +900,7 @@ public class AccountManagerService extends IAccountManager.Stub { } public void getAccountsByTypeAndFeatures(IAccountManagerResponse response, String type, String[] features) { + checkReadAccountsPermission(); if (type == null) { if (response != null) { try { @@ -929,7 +1102,12 @@ public class AccountManagerService extends IAccountManager.Stub { public void onResult(Bundle result) { mNumResults++; if (result != null && !TextUtils.isEmpty(result.getString(Constants.AUTHTOKEN_KEY))) { - cancelNotification(); + String accountName = result.getString(Constants.ACCOUNT_NAME_KEY); + String accountType = result.getString(Constants.ACCOUNT_TYPE_KEY); + if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { + Account account = new Account(accountName, accountType); + cancelNotification(getSigninRequiredNotificationId(account)); + } } IAccountManagerResponse response; if (mExpectActivityLaunch && result != null @@ -1026,6 +1204,8 @@ public class AccountManagerService extends IAccountManager.Stub { + AUTHTOKENS_AUTHTOKEN + " TEXT, " + "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))"); + createGrantsTable(db); + db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( " + EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + EXTRAS_ACCOUNTS_ID + " INTEGER, " @@ -1037,6 +1217,10 @@ public class AccountManagerService extends IAccountManager.Stub { + META_KEY + " TEXT PRIMARY KEY NOT NULL, " + META_VALUE + " TEXT)"); + createAccountsDeletionTrigger(db); + } + + private void createAccountsDeletionTrigger(SQLiteDatabase db) { db.execSQL("" + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS + " BEGIN" @@ -1044,22 +1228,34 @@ public class AccountManagerService extends IAccountManager.Stub { + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + " DELETE FROM " + TABLE_EXTRAS + " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_GRANTS + + " WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + " END"); } + private void createGrantsTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( " + + GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, " + + GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL, " + + GRANTS_GRANTEE_UID + " INTEGER NOT NULL, " + + "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE + + "," + GRANTS_GRANTEE_UID + "))"); + } + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); if (oldVersion == 1) { - db.execSQL("" - + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS - + " BEGIN" - + " DELETE FROM " + TABLE_AUTHTOKENS - + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + " =OLD." + ACCOUNTS_ID + " ;" - + " DELETE FROM " + TABLE_EXTRAS - + " WHERE " + EXTRAS_ACCOUNTS_ID + " =OLD." + ACCOUNTS_ID + " ;" - + " END"); + // no longer need to do anything since the work is done + // when upgrading from version 2 + oldVersion++; + } + + if (oldVersion == 2) { + createGrantsTable(db); + db.execSQL("DROP TRIGGER " + TABLE_ACCOUNTS + "Delete"); + createAccountsDeletionTrigger(db); oldVersion++; } } @@ -1156,31 +1352,171 @@ public class AccountManagerService extends IAccountManager.Stub { mAuthenticatorCache.dump(fd, fout, args); } - private void doNotification(CharSequence message, Intent intent) { + private void doNotification(Account account, CharSequence message, Intent intent) { long identityToken = clearCallingIdentity(); try { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "doNotification: " + message + " intent:" + intent); } - Notification n = new Notification(android.R.drawable.stat_sys_warning, null, - 0 /* when */); - n.setLatestEventInfo(mContext, mContext.getText(R.string.notification_title), message, - PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); - ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) - .notify(NOTIFICATION_ID, n); + if (intent.getComponent() != null && + GrantCredentialsPermissionActivity.class.getName().equals( + intent.getComponent().getClassName())) { + createNoCredentialsPermissionNotification(account, intent); + } else { + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + n.setLatestEventInfo(mContext, mContext.getText(R.string.notification_title), + message, PendingIntent.getActivity( + mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(getSigninRequiredNotificationId(account), n); + } } finally { restoreCallingIdentity(identityToken); } } - private void cancelNotification() { + private void cancelNotification(int id) { long identityToken = clearCallingIdentity(); try { ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) - .cancel(NOTIFICATION_ID); + .cancel(id); } finally { restoreCallingIdentity(identityToken); } } + + private void checkBinderPermission(String permission) { + final int uid = Binder.getCallingUid(); + if (mContext.checkCallingOrSelfPermission(permission) != + PackageManager.PERMISSION_GRANTED) { + String msg = "caller uid " + uid + " lacks " + permission; + Log.w(TAG, msg); + throw new SecurityException(msg); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "caller uid " + uid + " has " + permission); + } + } + + private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) { + final boolean fromAuthenticator = hasAuthenticatorUid(account.mType, callerUid); + final boolean hasExplicitGrants = hasExplicitlyGrantedPermission(account, authTokenType); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "checkGrantsOrCallingUidAgainstAuthenticator: caller uid " + + callerUid + ", account " + account + + ": is authenticator? " + fromAuthenticator + + ", has explicit permission? " + hasExplicitGrants); + } + return fromAuthenticator || hasExplicitGrants; + } + + private boolean hasAuthenticatorUid(String accountType, int callingUid) { + for (RegisteredServicesCache.ServiceInfo serviceInfo : + mAuthenticatorCache.getAllServices()) { + if (serviceInfo.type.type.equals(accountType)) { + return serviceInfo.uid == callingUid; + } + } + return false; + } + + private boolean hasExplicitlyGrantedPermission(Account account, String authTokenType) { + if (Binder.getCallingUid() == android.os.Process.SYSTEM_UID) { + return true; + } + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + String[] args = {String.valueOf(Binder.getCallingUid()), authTokenType, + account.mName, account.mType}; + final boolean permissionGranted = + DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args) != 0; + if (!permissionGranted && isDebuggableMonkeyBuild) { + // TODO: Skip this check when running automated tests. Replace this + // with a more general solution. + Log.w(TAG, "no credentials permission for usage of " + account + ", " + + authTokenType + " by uid " + Binder.getCallingUid() + + " but ignoring since this is a monkey build"); + return true; + } + return permissionGranted; + } + + private void checkCallingUidAgainstAuthenticator(Account account) { + final int uid = Binder.getCallingUid(); + if (!hasAuthenticatorUid(account.mType, uid)) { + String msg = "caller uid " + uid + " is different than the authenticator's uid"; + Log.w(TAG, msg); + throw new SecurityException(msg); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "caller uid " + uid + " is the same as the authenticator's uid"); + } + } + + private void checkAuthenticateAccountsPermission(Account account) { + checkBinderPermission(Manifest.permission.AUTHENTICATE_ACCOUNTS); + checkCallingUidAgainstAuthenticator(account); + } + + private void checkReadAccountsPermission() { + checkBinderPermission(Manifest.permission.GET_ACCOUNTS); + } + + private void checkManageAccountsPermission() { + checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS); + } + + /** + * Allow callers with the given uid permission to get credentials for account/authTokenType. + *

+ * Although this is public it can only be accessed via the AccountManagerService object + * which is in the system. This means we don't need to protect it with permissions. + * @hide + */ + public void grantAppPermission(Account account, String authTokenType, int uid) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId >= 0) { + ContentValues values = new ContentValues(); + values.put(GRANTS_ACCOUNTS_ID, accountId); + values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType); + values.put(GRANTS_GRANTEE_UID, uid); + db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values); + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid)); + } + + /** + * Don't allow callers with the given uid permission to get credentials for + * account/authTokenType. + *

+ * Although this is public it can only be accessed via the AccountManagerService object + * which is in the system. This means we don't need to protect it with permissions. + * @hide + */ + public void revokeAppPermission(Account account, String authTokenType, int uid) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId >= 0) { + db.delete(TABLE_GRANTS, + GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND " + + GRANTS_GRANTEE_UID + "=?", + new String[]{String.valueOf(accountId), authTokenType, + String.valueOf(uid)}); + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid)); + } } diff --git a/core/java/android/accounts/Constants.java b/core/java/android/accounts/Constants.java index bb7f9401e80a5..da8173f5f47b7 100644 --- a/core/java/android/accounts/Constants.java +++ b/core/java/android/accounts/Constants.java @@ -40,6 +40,7 @@ public class Constants { public static final String ACCOUNT_AUTHENTICATOR_RESPONSE_KEY = "accountAuthenticatorResponse"; public static final String ACCOUNT_MANAGER_RESPONSE_KEY = "accountManagerResponse"; public static final String AUTH_FAILED_MESSAGE_KEY = "authFailedMessage"; + public static final String AUTH_TOKEN_LABEL_KEY = "authTokenLabelKey"; public static final String AUTHENTICATOR_INTENT_ACTION = "android.accounts.AccountAuthenticator"; diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java new file mode 100644 index 0000000000000..f92d43f68e9bc --- /dev/null +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2009 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; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.TextView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.view.View; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import com.android.internal.R; + +/** + * @hide + */ +public class GrantCredentialsPermissionActivity extends Activity implements View.OnClickListener { + public static final String EXTRAS_ACCOUNT = "account"; + public static final String EXTRAS_AUTH_TOKEN_LABEL = "authTokenLabel"; + public static final String EXTRAS_AUTH_TOKEN_TYPE = "authTokenType"; + public static final String EXTRAS_RESPONSE = "response"; + public static final String EXTRAS_ACCOUNT_TYPE_LABEL = "accountTypeLabel"; + public static final String EXTRAS_PACKAGES = "application"; + public static final String EXTRAS_REQUESTING_UID = "uid"; + private Account mAccount; + private String mAuthTokenType; + private int mUid; + private Bundle mResultBundle = null; + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setContentView(R.layout.grant_credentials_permission); + mAccount = getIntent().getExtras().getParcelable(EXTRAS_ACCOUNT); + mAuthTokenType = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_TYPE); + mUid = getIntent().getExtras().getInt(EXTRAS_REQUESTING_UID); + final String accountTypeLabel = + getIntent().getExtras().getString(EXTRAS_ACCOUNT_TYPE_LABEL); + final String[] packages = getIntent().getExtras().getStringArray(EXTRAS_PACKAGES); + + findViewById(R.id.allow).setOnClickListener(this); + findViewById(R.id.deny).setOnClickListener(this); + + TextView messageView = (TextView) getWindow().findViewById(R.id.message); + String authTokenLabel = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_LABEL); + if (authTokenLabel.length() == 0) { + CharSequence grantCredentialsPermissionFormat = getResources().getText( + R.string.grant_credentials_permission_message_desc); + messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), + mAccount.mName, accountTypeLabel)); + } else { + CharSequence grantCredentialsPermissionFormat = getResources().getText( + R.string.grant_credentials_permission_message_with_authtokenlabel_desc); + messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), + authTokenLabel, mAccount.mName, accountTypeLabel)); + } + + String[] packageLabels = new String[packages.length]; + final PackageManager pm = getPackageManager(); + for (int i = 0; i < packages.length; i++) { + try { + packageLabels[i] = + pm.getApplicationLabel(pm.getApplicationInfo(packages[i], 0)).toString(); + } catch (PackageManager.NameNotFoundException e) { + packageLabels[i] = packages[i]; + } + } + ((ListView) findViewById(R.id.packages_list)).setAdapter( + new PackagesArrayAdapter(this, packageLabels)); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.allow: + AccountManagerService.getSingleton().grantAppPermission(mAccount, mAuthTokenType, + mUid); + Intent result = new Intent(); + result.putExtra("retry", true); + setResult(RESULT_OK, result); + setAccountAuthenticatorResult(result.getExtras()); + break; + + case R.id.deny: + AccountManagerService.getSingleton().revokeAppPermission(mAccount, mAuthTokenType, + mUid); + setResult(RESULT_CANCELED); + break; + } + finish(); + } + + public final void setAccountAuthenticatorResult(Bundle result) { + mResultBundle = result; + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + Intent intent = getIntent(); + AccountAuthenticatorResponse accountAuthenticatorResponse = + intent.getParcelableExtra(EXTRAS_RESPONSE); + if (accountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + accountAuthenticatorResponse.onResult(mResultBundle); + } else { + accountAuthenticatorResponse.onError(Constants.ERROR_CODE_CANCELED, "canceled"); + } + } + super.finish(); + } + + private static class PackagesArrayAdapter extends ArrayAdapter { + protected LayoutInflater mInflater; + private static final int mResource = R.layout.simple_list_item_1; + + public PackagesArrayAdapter(Context context, String[] items) { + super(context, mResource, items); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + static class ViewHolder { + TextView label; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // A ViewHolder keeps references to children views to avoid unneccessary calls + // to findViewById() on each row. + ViewHolder holder; + + // When convertView is not null, we can reuse it directly, there is no need + // to reinflate it. We only inflate a new View when the convertView supplied + // by ListView is null. + if (convertView == null) { + convertView = mInflater.inflate(mResource, null); + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + holder = new ViewHolder(); + holder.label = (TextView) convertView.findViewById(R.id.text1); + + convertView.setTag(holder); + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + holder = (ViewHolder) convertView.getTag(); + } + + holder.label.setText(getItem(position)); + + return convertView; + } + } +} diff --git a/core/java/android/accounts/IAccountAuthenticator.aidl b/core/java/android/accounts/IAccountAuthenticator.aidl index 46a714468fb5a..7d4de392b6ef6 100644 --- a/core/java/android/accounts/IAccountAuthenticator.aidl +++ b/core/java/android/accounts/IAccountAuthenticator.aidl @@ -48,6 +48,11 @@ oneway interface IAccountAuthenticator { void getAuthToken(in IAccountAuthenticatorResponse response, in Account account, String authTokenType, in Bundle options); + /** + * Gets the user-visible label of the given authtoken type. + */ + void getAuthTokenLabel(in IAccountAuthenticatorResponse response, String authTokenType); + /** * prompts the user for a new password and writes it to the IAccountManager */ diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index bb94372b09a23..342de2b43186a 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -114,10 +114,12 @@ public abstract class RegisteredServicesCache { public static class ServiceInfo { public final V type; public final ComponentName componentName; + public final int uid; - private ServiceInfo(V type, ComponentName componentName) { + private ServiceInfo(V type, ComponentName componentName, int uid) { this.type = type; this.componentName = componentName; + this.uid = uid; } public String toString() { @@ -223,7 +225,10 @@ public abstract class RegisteredServicesCache { if (v == null) { return null; } - return new ServiceInfo(v, componentName); + final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo; + final ApplicationInfo applicationInfo = serviceInfo.applicationInfo; + final int uid = applicationInfo.uid; + return new ServiceInfo(v, componentName, uid); } finally { if (parser != null) parser.close(); } diff --git a/core/java/android/util/Pair.java b/core/java/android/util/Pair.java new file mode 100644 index 0000000000000..bf25306540fd5 --- /dev/null +++ b/core/java/android/util/Pair.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2009 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.util; + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +public class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. If either are null then equals() and hashCode() will throw + * a NullPointerException. + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective equals() methods. + * @param o the Pair to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered equals() + */ + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Pair)) return false; + final Pair other; + try { + other = (Pair) o; + } catch (ClassCastException e) { + return false; + } + return first.equals(other.first) && second.equals(other.second); + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * @return a hashcode of the Pair + */ + public int hashCode() { + int result = 17; + result = 31 * result + first.hashCode(); + result = 31 * result + second.hashCode(); + return result; + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 027fb2314af0f..9d55dab95f79c 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -25,7 +25,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -309,6 +309,14 @@ android:description="@string/permdesc_bluetooth" android:label="@string/permlab_bluetooth" /> + + + @@ -330,6 +338,28 @@ android:description="@string/permdesc_getAccounts" android:label="@string/permlab_getAccounts" /> + + + + + + + + + @@ -1093,6 +1123,11 @@ android:exported="true"> + + + diff --git a/core/res/res/layout/grant_credentials_permission.xml b/core/res/res/layout/grant_credentials_permission.xml new file mode 100644 index 0000000000000..fe1c22ed5b2d4 --- /dev/null +++ b/core/res/res/layout/grant_credentials_permission.xml @@ -0,0 +1,41 @@ + + + + + +