1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.sync.test.util;
6
7
8import android.accounts.Account;
9import android.content.ContentResolver;
10import android.content.SyncStatusObserver;
11import android.os.AsyncTask;
12
13import junit.framework.Assert;
14
15import org.chromium.base.ThreadUtils;
16import org.chromium.sync.notifier.SyncContentResolverDelegate;
17
18import java.util.HashMap;
19import java.util.HashSet;
20import java.util.Map;
21import java.util.Set;
22import java.util.concurrent.Semaphore;
23import java.util.concurrent.TimeUnit;
24
25
26/**
27 * Mock implementation of the {@link SyncContentResolverDelegate}.
28 *
29 * This implementation only supports status change listeners for the type
30 * SYNC_OBSERVER_TYPE_SETTINGS.
31 */
32public class MockSyncContentResolverDelegate implements SyncContentResolverDelegate {
33
34    private final Set<String> mSyncAutomaticallySet;
35    private final Map<String, Boolean> mIsSyncableMap;
36    private final Object mSyncableMapLock = new Object();
37
38    private final Set<AsyncSyncStatusObserver> mObservers;
39
40    private boolean mMasterSyncAutomatically;
41    private boolean mDisableObserverNotifications;
42
43    private Semaphore mPendingObserverCount;
44
45    public MockSyncContentResolverDelegate() {
46        mSyncAutomaticallySet = new HashSet<String>();
47        mIsSyncableMap = new HashMap<String, Boolean>();
48        mObservers = new HashSet<AsyncSyncStatusObserver>();
49    }
50
51    @Override
52    public Object addStatusChangeListener(int mask, SyncStatusObserver callback) {
53        if (mask != ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) {
54            throw new IllegalArgumentException("This implementation only supports "
55                    + "ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS as the mask");
56        }
57        AsyncSyncStatusObserver asyncSyncStatusObserver = new AsyncSyncStatusObserver(callback);
58        synchronized (mObservers) {
59            mObservers.add(asyncSyncStatusObserver);
60        }
61        return asyncSyncStatusObserver;
62    }
63
64    @Override
65    public void removeStatusChangeListener(Object handle) {
66        synchronized (mObservers) {
67            mObservers.remove(handle);
68        }
69    }
70
71    @Override
72    public void setMasterSyncAutomatically(boolean sync) {
73        if (mMasterSyncAutomatically == sync) return;
74
75        mMasterSyncAutomatically = sync;
76        notifyObservers();
77    }
78
79    @Override
80    public boolean getMasterSyncAutomatically() {
81        return mMasterSyncAutomatically;
82    }
83
84    @Override
85    public boolean getSyncAutomatically(Account account, String authority) {
86        String key = createKey(account, authority);
87        synchronized (mSyncableMapLock) {
88            return mSyncAutomaticallySet.contains(key);
89        }
90    }
91
92    @Override
93    public void setSyncAutomatically(Account account, String authority, boolean sync) {
94        String key = createKey(account, authority);
95        synchronized (mSyncableMapLock) {
96            if (!mIsSyncableMap.containsKey(key) || !mIsSyncableMap.get(key)) {
97                throw new IllegalArgumentException("Account " + account +
98                        " is not syncable for authority " + authority +
99                        ". Can not set sync state to " + sync);
100            }
101            if (sync) {
102                mSyncAutomaticallySet.add(key);
103            } else if (mSyncAutomaticallySet.contains(key)) {
104                mSyncAutomaticallySet.remove(key);
105            }
106        }
107        notifyObservers();
108    }
109
110    @Override
111    public void setIsSyncable(Account account, String authority, int syncable) {
112        String key = createKey(account, authority);
113
114        synchronized (mSyncableMapLock) {
115            switch (syncable) {
116                case 0:
117                    if (mSyncAutomaticallySet.contains(key)) {
118                        mSyncAutomaticallySet.remove(key);
119                    }
120
121                    mIsSyncableMap.put(key, false);
122                    break;
123                case 1:
124                    mIsSyncableMap.put(key, true);
125                    break;
126                case -1:
127                    if (mIsSyncableMap.containsKey(key)) {
128                        mIsSyncableMap.remove(key);
129                    }
130                    break;
131                default:
132                    throw new IllegalArgumentException("Unable to understand syncable argument: " +
133                            syncable);
134            }
135        }
136        notifyObservers();
137    }
138
139    @Override
140    public int getIsSyncable(Account account, String authority) {
141        String key = createKey(account, authority);
142        synchronized (mSyncableMapLock) {
143            if (mIsSyncableMap.containsKey(key)) {
144                return mIsSyncableMap.containsKey(key) ? 1 : 0;
145            } else {
146                return -1;
147            }
148        }
149    }
150
151    private static String createKey(Account account, String authority) {
152        return account.name + "@@@" + account.type + "@@@" + authority;
153    }
154
155    private void notifyObservers() {
156        if (mDisableObserverNotifications) return;
157        synchronized (mObservers) {
158            mPendingObserverCount = new Semaphore(1 - mObservers.size());
159            for (AsyncSyncStatusObserver observer : mObservers) {
160                observer.notifyObserverAsync(mPendingObserverCount);
161            }
162        }
163    }
164
165    /**
166     * Blocks until the last notification has been issued to all registered observers.
167     * Note that if an observer is removed while a notification is being handled this can
168     * fail to return correctly.
169     *
170     * @throws InterruptedException
171     */
172    public void waitForLastNotificationCompleted() throws InterruptedException {
173        Assert.assertTrue("Timed out waiting for notifications to complete.",
174                mPendingObserverCount.tryAcquire(5, TimeUnit.SECONDS));
175    }
176
177    public void disableObserverNotifications() {
178        mDisableObserverNotifications = true;
179    }
180
181    private static class AsyncSyncStatusObserver {
182
183        private final SyncStatusObserver mSyncStatusObserver;
184
185        private AsyncSyncStatusObserver(SyncStatusObserver syncStatusObserver) {
186            mSyncStatusObserver = syncStatusObserver;
187        }
188
189        private void notifyObserverAsync(final Semaphore pendingObserverCount) {
190            if (ThreadUtils.runningOnUiThread()) {
191                new AsyncTask<Void, Void, Void>() {
192                    @Override
193                    protected Void doInBackground(Void... params) {
194                        mSyncStatusObserver.onStatusChanged(
195                                ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
196                        return null;
197                    }
198
199                    @Override
200                    protected void onPostExecute(Void result) {
201                        pendingObserverCount.release();
202                    }
203                }.execute();
204            } else {
205                mSyncStatusObserver.onStatusChanged(
206                        ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
207                pendingObserverCount.release();
208            }
209        }
210    }
211}
212