• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.google.android.mobly.snippet.bundled;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.accounts.AccountManagerFuture;
22 import android.accounts.AccountsException;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.SyncAdapterType;
26 import android.os.Build;
27 import android.os.Bundle;
28 import androidx.annotation.RequiresApi;
29 import androidx.test.platform.app.InstrumentationRegistry;
30 import com.google.android.mobly.snippet.Snippet;
31 import com.google.android.mobly.snippet.rpc.Rpc;
32 import com.google.android.mobly.snippet.util.Log;
33 import java.io.IOException;
34 import java.util.Arrays;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.TreeSet;
43 import java.util.concurrent.locks.ReentrantReadWriteLock;
44 
45 /**
46  * Snippet class exposing Android APIs related to management of device accounts.
47  *
48  * <p>Android devices can have accounts of any type added and synced. New types can be created by
49  * apps by implementing a {@link android.content.ContentProvider} for a particular account type.
50  *
51  * <p>Google (gmail) accounts are of type "com.google" and their handling is managed by the
52  * operating system. This class allows you to add and remove Google accounts from a device.
53  */
54 public class AccountSnippet implements Snippet {
55     private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
56     private static final String AUTH_TOKEN_TYPE = "mail";
57 
58     private static class AccountSnippetException extends Exception {
59         private static final long serialVersionUID = 1;
60 
AccountSnippetException(String msg)61         public AccountSnippetException(String msg) {
62             super(msg);
63         }
64     }
65 
66     private final AccountManager mAccountManager;
67     private final List<Object> mSyncStatusObserverHandles;
68 
69     private final Map<String, Set<String>> mSyncAllowList;
70     private final ReentrantReadWriteLock mLock;
71 
AccountSnippet()72     public AccountSnippet() {
73         Context context = InstrumentationRegistry.getInstrumentation().getContext();
74         mAccountManager = AccountManager.get(context);
75         mSyncStatusObserverHandles = new LinkedList<>();
76         mSyncAllowList = new HashMap<>();
77         mLock = new ReentrantReadWriteLock();
78     }
79 
80     /**
81      * Adds a Google account to the device.
82      *
83      * @param username Username of the account to add (including @gmail.com).
84      * @param password Password of the account to add.
85      */
86     @Rpc(
87             description =
88                     "Add a Google (GMail) account to the device, with account data sync disabled.")
addAccount(String username, String password)89     public void addAccount(String username, String password)
90             throws AccountSnippetException, AccountsException, IOException {
91         // Check for existing account. If we try to re-add an existing account, Android throws an
92         // exception that says "Account does not exist or not visible. Maybe change pwd?" which is
93         // a little hard to understand.
94         if (listAccounts().contains(username)) {
95             throw new AccountSnippetException(
96                     "Account " + username + " already exists on the device");
97         }
98         Bundle addAccountOptions = new Bundle();
99         addAccountOptions.putString("username", username);
100         addAccountOptions.putString("password", password);
101         AccountManagerFuture<Bundle> future =
102                 mAccountManager.addAccount(
103                         GOOGLE_ACCOUNT_TYPE,
104                         AUTH_TOKEN_TYPE,
105                         null /* requiredFeatures */,
106                         addAccountOptions,
107                         null /* activity */,
108                         null /* authCallback */,
109                         null /* handler */);
110         Bundle result = future.getResult();
111         if (result.containsKey(AccountManager.KEY_ERROR_CODE)) {
112             throw new AccountSnippetException(
113                     String.format(
114                             Locale.US,
115                             "Failed to add account due to code %d: %s",
116                             result.getInt(AccountManager.KEY_ERROR_CODE),
117                             result.getString(AccountManager.KEY_ERROR_MESSAGE)));
118         }
119 
120         // Disable sync to avoid test flakiness as accounts fetch additional data.
121         // It takes a while for all sync adapters to be populated, so register for broadcasts when
122         // sync is starting and disable them there.
123         // NOTE: this listener is NOT unregistered because several sync requests for the new account
124         // will come in over time.
125         Account account = new Account(username, GOOGLE_ACCOUNT_TYPE);
126         Object handle =
127                 ContentResolver.addStatusChangeListener(
128                         ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
129                                 | ContentResolver.SYNC_OBSERVER_TYPE_PENDING,
130                         which -> {
131                             for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) {
132                                 // Ignore non-Google account types.
133                                 if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) {
134                                     continue;
135                                 }
136                                 // If a content provider is not allowListed, then disable it.
137                                 // Because startSync and stopSync synchronously update the allowList
138                                 // and sync settings, writelock both the allowList check and the
139                                 // call to sync together.
140                                 mLock.writeLock().lock();
141                                 try {
142                                     if (!isAdapterAllowListed(username, adapter.authority)) {
143                                         updateSync(account, adapter.authority, false /* sync */);
144                                     }
145                                 } finally {
146                                     mLock.writeLock().unlock();
147                                 }
148                             }
149                         });
150         mSyncStatusObserverHandles.add(handle);
151     }
152 
153     /**
154      * Removes an account from the device.
155      *
156      * <p>The account has to be Google account.
157      *
158      * @param username the username of the account to remove.
159      * @throws AccountSnippetException if removing the account failed.
160      */
161     @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
162     @Rpc(description = "Remove a Google account.")
removeAccount(String username)163     public void removeAccount(String username) throws AccountSnippetException {
164         if (!mAccountManager.removeAccountExplicitly(getAccountByName(username))) {
165             throw new AccountSnippetException("Failed to remove account '" + username + "'.");
166         }
167     }
168 
169     /**
170      * Get an existing account by its username.
171      *
172      * <p>Google account only.
173      *
174      * @param username the username of the account to remove.
175      * @return tHe account with the username.
176      * @throws AccountSnippetException if no account has the given username.
177      */
getAccountByName(String username)178     private Account getAccountByName(String username) throws AccountSnippetException {
179         Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE);
180         for (Account account : accounts) {
181             if (account.name.equals(username)) {
182                 return account;
183             }
184         }
185         throw new AccountSnippetException(
186                 "Account '" + username + "' does not exist on the device.");
187     }
188 
189     /**
190      * Checks to see if the SyncAdapter is allowListed.
191      *
192      * <p>AccountSnippet disables syncing by default when adding an account, except for allowListed
193      * SyncAdapters. This function checks the allowList for a specific account-authority pair.
194      *
195      * @param username Username of the account (including @gmail.com).
196      * @param authority The authority of a content provider that should be checked.
197      */
isAdapterAllowListed(String username, String authority)198     private boolean isAdapterAllowListed(String username, String authority) {
199         boolean result = false;
200         mLock.readLock().lock();
201         try {
202             Set<String> allowListedProviders = mSyncAllowList.get(username);
203             if (allowListedProviders != null) {
204                 result = allowListedProviders.contains(authority);
205             }
206         } finally {
207             mLock.readLock().unlock();
208         }
209         return result;
210     }
211 
212     /**
213      * Updates ContentResolver sync settings for an Account's specified SyncAdapter.
214      *
215      * <p>Sets an accounts SyncAdapter (selected based on authority) to sync/not-sync automatically
216      * and immediately requests/cancels a sync.
217      *
218      * <p>updateSync should always be called under {@link AccountSnippet#mLock} write lock to avoid
219      * flapping between the getSyncAutomatically and setSyncAutomatically calls.
220      *
221      * @param account A Google Account.
222      * @param authority The authority of a content provider that should (not) be synced.
223      * @param sync Whether or not the account's content provider should be synced.
224      */
updateSync(Account account, String authority, boolean sync)225     private void updateSync(Account account, String authority, boolean sync) {
226         if (ContentResolver.getSyncAutomatically(account, authority) != sync) {
227             ContentResolver.setSyncAutomatically(account, authority, sync);
228             if (sync) {
229                 ContentResolver.requestSync(account, authority, new Bundle());
230             } else {
231                 ContentResolver.cancelSync(account, authority);
232             }
233             Log.i(
234                     "Set sync to "
235                             + sync
236                             + " for account "
237                             + account
238                             + ", adapter "
239                             + authority
240                             + ".");
241         }
242     }
243 
244     /**
245      * Enables syncing of a SyncAdapter for a given content provider.
246      *
247      * <p>Adds the authority to a allowList, and immediately requests a sync.
248      *
249      * @param username Username of the account (including @gmail.com).
250      * @param authority The authority of a content provider that should be synced.
251      */
252     @Rpc(description = "Enables syncing of a SyncAdapter for a content provider.")
startSync(String username, String authority)253     public void startSync(String username, String authority) throws AccountSnippetException {
254         if (!listAccounts().contains(username)) {
255             throw new AccountSnippetException("Account " + username + " is not on the device");
256         }
257         // Add to the allowList
258         mLock.writeLock().lock();
259         try {
260             if (mSyncAllowList.containsKey(username)) {
261                 mSyncAllowList.get(username).add(authority);
262             } else {
263                 mSyncAllowList.put(username, new HashSet<String>(Arrays.asList(authority)));
264             }
265             // Update the Sync settings
266             for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) {
267                 // Find the Google account content provider.
268                 if (adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)
269                         && adapter.authority.equals(authority)) {
270                     Account account = new Account(username, GOOGLE_ACCOUNT_TYPE);
271                     updateSync(account, authority, true);
272                 }
273             }
274         } finally {
275             mLock.writeLock().unlock();
276         }
277     }
278 
279     /**
280      * Disables syncing of a SyncAdapter for a given content provider.
281      *
282      * <p>Removes the content provider authority from a allowList.
283      *
284      * @param username Username of the account (including @gmail.com).
285      * @param authority The authority of a content provider that should not be synced.
286      */
287     @Rpc(description = "Disables syncing of a SyncAdapter for a content provider.")
stopSync(String username, String authority)288     public void stopSync(String username, String authority) throws AccountSnippetException {
289         if (!listAccounts().contains(username)) {
290             throw new AccountSnippetException("Account " + username + " is not on the device");
291         }
292         // Remove from allowList
293         mLock.writeLock().lock();
294         try {
295             if (mSyncAllowList.containsKey(username)) {
296                 Set<String> allowListedProviders = mSyncAllowList.get(username);
297                 allowListedProviders.remove(authority);
298                 if (allowListedProviders.isEmpty()) {
299                     mSyncAllowList.remove(username);
300                 }
301             }
302             // Update the Sync settings
303             for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) {
304                 // Find the Google account content provider.
305                 if (adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)
306                         && adapter.authority.equals(authority)) {
307                     Account account = new Account(username, GOOGLE_ACCOUNT_TYPE);
308                     updateSync(account, authority, false);
309                 }
310             }
311         } finally {
312             mLock.writeLock().unlock();
313         }
314     }
315 
316     /**
317      * Returns a list of all Google accounts on the device.
318      *
319      * <p>TODO(adorokhine): Support accounts of other types with an optional 'type' kwarg.
320      */
321     @Rpc(description = "List all Google (GMail) accounts on the device.")
listAccounts()322     public Set<String> listAccounts() throws SecurityException {
323         Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE);
324         Set<String> usernames = new TreeSet<>();
325         for (Account account : accounts) {
326             usernames.add(account.name);
327         }
328         return usernames;
329     }
330 
331     @Override
shutdown()332     public void shutdown() {
333         for (Object handle : mSyncStatusObserverHandles) {
334             ContentResolver.removeStatusChangeListener(handle);
335         }
336     }
337 }
338