/* * Copyright (C) 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.android.mobly.snippet.bundled; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerFuture; import android.accounts.AccountsException; import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; import android.os.Bundle; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.util.Log; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Snippet class exposing Android APIs related to management of device accounts. * *

Android devices can have accounts of any type added and synced. New types can be created by * apps by implementing a {@link android.content.ContentProvider} for a particular account type. * *

Google (gmail) accounts are of type "com.google" and their handling is managed by the * operating system. This class allows you to add and remove Google accounts from a device. */ public class AccountSnippet implements Snippet { private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; private static final String AUTH_TOKEN_TYPE = "mail"; private static class AccountSnippetException extends Exception { private static final long serialVersionUID = 1; public AccountSnippetException(String msg) { super(msg); } } private final AccountManager mAccountManager; private final List mSyncStatusObserverHandles; private final Map> mSyncWhitelist; private final ReentrantReadWriteLock mLock; public AccountSnippet() { Context context = InstrumentationRegistry.getContext(); mAccountManager = AccountManager.get(context); mSyncStatusObserverHandles = new LinkedList<>(); mSyncWhitelist = new HashMap<>(); mLock = new ReentrantReadWriteLock(); } /** * Adds a Google account to the device. * *

TODO(adorokhine): Support adding accounts of other types with an optional 'type' kwarg. * *

TODO(adorokhine): Allow users to choose whether to enable/disable sync with a kwarg. * * @param username Username of the account to add (including @gmail.com). * @param password Password of the account to add. */ @Rpc( description = "Add a Google (GMail) account to the device, with account data sync disabled." ) public void addAccount(String username, String password) throws AccountSnippetException, AccountsException, IOException { // Check for existing account. If we try to re-add an existing account, Android throws an // exception that says "Account does not exist or not visible. Maybe change pwd?" which is // a little hard to understand. if (listAccounts().contains(username)) { throw new AccountSnippetException( "Account " + username + " already exists on the device"); } Bundle addAccountOptions = new Bundle(); addAccountOptions.putString("username", username); addAccountOptions.putString("password", password); AccountManagerFuture future = mAccountManager.addAccount( GOOGLE_ACCOUNT_TYPE, AUTH_TOKEN_TYPE, null /* requiredFeatures */, addAccountOptions, null /* activity */, null /* authCallback */, null /* handler */); Bundle result = future.getResult(); if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { throw new AccountSnippetException( String.format( "Failed to add account due to code %d: %s", result.getInt(AccountManager.KEY_ERROR_CODE), result.getString(AccountManager.KEY_ERROR_MESSAGE))); } // Disable sync to avoid test flakiness as accounts fetch additional data. // It takes a while for all sync adapters to be populated, so register for broadcasts when // sync is starting and disable them there. // NOTE: this listener is NOT unregistered because several sync requests for the new account // will come in over time. Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); Object handle = ContentResolver.addStatusChangeListener( ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, which -> { for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { // Ignore non-Google account types. if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) { continue; } // If a content provider is not whitelisted, then disable it. // Because startSync and stopSync synchronously update the whitelist // and sync settings, writelock both the whitelist check and the // call to sync together. mLock.writeLock().lock(); try { if (!isAdapterWhitelisted(username, adapter.authority)) { updateSync(account, adapter.authority, false /* sync */); } } finally { mLock.writeLock().unlock(); } } }); mSyncStatusObserverHandles.add(handle); } /** * Checks to see if the SyncAdapter is whitelisted. * *

AccountSnippet disables syncing by default when adding an account, except for whitelisted * SyncAdapters. This function checks the whitelist for a specific account-authority pair. * * @param username Username of the account (including @gmail.com). * @param authority The authority of a content provider that should be checked. */ private boolean isAdapterWhitelisted(String username, String authority) { boolean result = false; mLock.readLock().lock(); try { Set whitelistedProviders = mSyncWhitelist.get(username); if (whitelistedProviders != null) { result = whitelistedProviders.contains(authority); } } finally { mLock.readLock().unlock(); } return result; } /** * Updates ContentResolver sync settings for an Account's specified SyncAdapter. * *

Sets an accounts SyncAdapter (selected based on authority) to sync/not-sync automatically * and immediately requests/cancels a sync. * *

updateSync should always be called under {@link AccountSnippet#mLock} write lock to avoid * flapping between the getSyncAutomatically and setSyncAutomatically calls. * * @param account A Google Account. * @param authority The authority of a content provider that should (not) be synced. * @param sync Whether or not the account's content provider should be synced. */ private void updateSync(Account account, String authority, boolean sync) { if (ContentResolver.getSyncAutomatically(account, authority) != sync) { ContentResolver.setSyncAutomatically(account, authority, sync); if (sync) { ContentResolver.requestSync(account, authority, new Bundle()); } else { ContentResolver.cancelSync(account, authority); } Log.i( "Set sync to " + sync + " for account " + account + ", adapter " + authority + "."); } } /** * Enables syncing of a SyncAdapter for a given content provider. * *

Adds the authority to a whitelist, and immediately requests a sync. * * @param username Username of the account (including @gmail.com). * @param authority The authority of a content provider that should be synced. */ @Rpc(description = "Enables syncing of a SyncAdapter for a content provider.") public void startSync(String username, String authority) throws AccountSnippetException { if (!listAccounts().contains(username)) { throw new AccountSnippetException("Account " + username + " is not on the device"); } // Add to the whitelist mLock.writeLock().lock(); try { if (mSyncWhitelist.containsKey(username)) { mSyncWhitelist.get(username).add(authority); } else { mSyncWhitelist.put(username, new HashSet(Arrays.asList(authority))); } // Update the Sync settings for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { // Find the Google account content provider. if (adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE) && adapter.authority.equals(authority)) { Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); updateSync(account, authority, true); } } } finally { mLock.writeLock().unlock(); } } /** * Disables syncing of a SyncAdapter for a given content provider. * *

Removes the content provider authority from a whitelist. * * @param username Username of the account (including @gmail.com). * @param authority The authority of a content provider that should not be synced. */ @Rpc(description = "Disables syncing of a SyncAdapter for a content provider.") public void stopSync(String username, String authority) throws AccountSnippetException { if (!listAccounts().contains(username)) { throw new AccountSnippetException("Account " + username + " is not on the device"); } // Remove from whitelist mLock.writeLock().lock(); try { if (mSyncWhitelist.containsKey(username)) { Set whitelistedProviders = mSyncWhitelist.get(username); whitelistedProviders.remove(authority); if (whitelistedProviders.isEmpty()) { mSyncWhitelist.remove(username); } } // Update the Sync settings for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { // Find the Google account content provider. if (adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE) && adapter.authority.equals(authority)) { Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); updateSync(account, authority, false); } } } finally { mLock.writeLock().unlock(); } } /** * Returns a list of all Google accounts on the device. * *

TODO(adorokhine): Support accounts of other types with an optional 'type' kwarg. */ @Rpc(description = "List all Google (GMail) accounts on the device.") public Set listAccounts() throws SecurityException { Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); Set usernames = new TreeSet<>(); for (Account account : accounts) { usernames.add(account.name); } return usernames; } @Override public void shutdown() { for (Object handle : mSyncStatusObserverHandles) { ContentResolver.removeStatusChangeListener(handle); } } }