1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.settings.accounts;
18
19import com.android.tv.settings.R;
20import com.android.tv.settings.dialog.old.Action;
21import com.android.tv.settings.dialog.old.ActionAdapter;
22import com.android.tv.settings.dialog.old.ActionFragment;
23import com.android.tv.settings.dialog.old.ContentFragment;
24import com.android.tv.settings.dialog.old.DialogActivity;
25
26import android.accounts.Account;
27import android.accounts.AccountManager;
28import android.accounts.OnAccountsUpdateListener;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.Intent;
32import android.content.SyncAdapterType;
33import android.content.SyncInfo;
34import android.content.SyncStatusInfo;
35import android.content.SyncStatusObserver;
36import android.content.pm.ProviderInfo;
37import android.net.ConnectivityManager;
38import android.os.AsyncTask;
39import android.os.Bundle;
40import android.preference.Preference;
41import android.text.TextUtils;
42import android.text.format.DateUtils;
43import android.util.Log;
44
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.Comparator;
48import java.util.Date;
49import java.util.HashMap;
50import java.util.List;
51
52/**
53 * Displays the sync settings for a given account.
54 */
55public class AccountSyncSettings extends DialogActivity implements OnAccountsUpdateListener {
56
57    private static final String TAG = "AccountSyncSettings";
58
59    private static final String KEY_SYNC_NOW = "KEY_SYNC_NOW";
60    private static final String KEY_CANCEL_SYNC = "KEY_SYNC_CANCEL";
61
62    private static final String EXTRA_ONE_TIME_SYNC = "one_time_sync";
63    private static final String EXTRA_ACCOUNT = "account";
64
65    private AccountManager mAccountManager;
66    private Account mAccount;
67    private AuthenticatorHelper mHelper;
68
69    /**
70     * Adapters which are invisible. Store them so that sync now syncs everything.
71     */
72    private List<SyncAdapterType> mInvisibleAdapters;
73
74    private Account[] mAccounts;
75
76    private boolean mSyncIsFailing;
77
78    private Object mStatusChangeListenerHandle;
79
80    private ActionFragment mActionFragment;
81
82    private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
83
84        @Override
85        public void onStatusChanged(int which) {
86            onSyncStateUpdated();
87        }
88    };
89
90    @Override
91    protected void onCreate(Bundle savedInstanceState) {
92        super.onCreate(savedInstanceState);
93        mHelper = new AuthenticatorHelper();
94        mAccountManager = AccountManager.get(this);
95        mInvisibleAdapters = new ArrayList<SyncAdapterType>();
96        String accountName = getIntent().getStringExtra(AccountSettingsActivity.EXTRA_ACCOUNT);
97        if (!TextUtils.isEmpty(accountName)) {
98            // Search for the account.
99            for (Account account : mAccountManager.getAccounts()) {
100                if (account.name.equals(accountName)) {
101                    mAccount = account;
102                    break;
103                }
104            }
105        }
106        if (mAccount == null) {
107            finish();
108            return;
109        }
110        mActionFragment = ActionFragment.newInstance(getActions(mAccountManager.getAccounts()));
111        // Start with an empty list and then fill in with the sync adapters.
112        setContentAndActionFragments(ContentFragment.newInstance(
113                accountName, mAccount.type, "", R.drawable.ic_settings_sync,
114                getResources().getColor(R.color.icon_background)), mActionFragment);
115    }
116
117    @Override
118    protected void onResume() {
119        super.onResume();
120        AccountManager.get(this).addOnAccountsUpdatedListener(this, null, false);
121        updateAuthDescriptions();
122        onAccountsUpdated(AccountManager.get(this).getAccounts());
123        mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
124                ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
125                | ContentResolver.SYNC_OBSERVER_TYPE_STATUS
126                | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
127    }
128
129    @Override
130    public void onPause() {
131        ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
132        AccountManager.get(this).removeOnAccountsUpdatedListener(this);
133        super.onPause();
134    }
135
136    @Override
137    public void onAccountsUpdated(Account[] accounts) {
138        mAccounts = accounts;
139        loadSyncActions(accounts);
140    }
141
142    @Override
143    public void onActionClicked(Action action) {
144        String key = action.getKey();
145        if (KEY_SYNC_NOW.equals(key)) {
146            startSyncForEnabledProviders();
147        } else if (KEY_CANCEL_SYNC.equals(key)) {
148            cancelSyncForEnabledProviders();
149        } else {
150            // This is a specific sync adapter.
151            Account account = action.getIntent().getParcelableExtra(EXTRA_ACCOUNT);
152            String authority = action.getKey();
153            boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority);
154            if (action.getIntent().getBooleanExtra(EXTRA_ONE_TIME_SYNC, false)) {
155                requestOrCancelSync(account, authority, true);
156            } else {
157                boolean syncOn = !action.isChecked(); // toggle
158                boolean oldSyncState = syncAutomatically;
159                if (syncOn != oldSyncState) {
160                    // if we're enabling sync, this will request a sync as well
161                    ContentResolver.setSyncAutomatically(account, authority, syncOn);
162                    // if the master sync switch is off, the request above will
163                    // get dropped.  when the user clicks on this toggle,
164                    // we want to force the sync, however.
165                    if (!ContentResolver.getMasterSyncAutomatically() || !syncOn) {
166                        requestOrCancelSync(account, authority, syncOn);
167                    }
168                }
169                if (mAccounts != null) {
170                    loadSyncActions(mAccounts);
171                }
172            }
173        }
174    }
175
176    private void onSyncStateUpdated() {
177        if (!isResumed() || mAccounts == null) {
178            return;
179        }
180        loadSyncActions(mAccounts);
181    }
182
183    private void updateAuthDescriptions() {
184        mHelper.updateAuthDescriptions(this);
185        onAuthDescriptionsUpdated();
186    }
187
188    private void onAuthDescriptionsUpdated() {
189        ((ContentFragment) getContentFragment()).setBreadCrumbText(
190                mHelper.getLabelForType(this, mAccount.type).toString());
191    }
192
193
194    private void loadSyncActions(Account[] accounts) {
195        new LoadActionsTask().execute(accounts);
196    }
197
198    private ArrayList<Action> getActions(Account[] accounts) {
199        ArrayList<Action> actions = new ArrayList<Action>();
200
201        Date date = new Date();
202        List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncs();
203        mSyncIsFailing = false;
204
205        mInvisibleAdapters.clear();
206
207        SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes();
208        HashMap<String, ArrayList<String>> accountTypeToAuthorities =
209                new HashMap<String, ArrayList<String>>();
210        for (int i = 0, n = syncAdapters.length; i < n; i++) {
211            final SyncAdapterType sa = syncAdapters[i];
212            if (sa.isUserVisible()) {
213                ArrayList<String> authorities = accountTypeToAuthorities.get(sa.accountType);
214                if (authorities == null) {
215                    authorities = new ArrayList<String>();
216                    accountTypeToAuthorities.put(sa.accountType, authorities);
217                }
218                if (Log.isLoggable(TAG, Log.VERBOSE)) {
219                    Log.d(TAG, "onAccountUpdated: added authority " + sa.authority
220                            + " to accountType " + sa.accountType);
221                }
222                authorities.add(sa.authority);
223            } else {
224                // keep track of invisible sync adapters, so sync now forces
225                // them to sync as well.
226                mInvisibleAdapters.add(sa);
227            }
228        }
229
230        for (int i = 0, n = accounts.length; i < n; i++) {
231            final Account account = accounts[i];
232            if (Log.isLoggable(TAG, Log.VERBOSE)) {
233                Log.d(TAG, "looking for sync adapters that match account " + account);
234            }
235            final ArrayList<String> authorities = accountTypeToAuthorities.get(account.type);
236            if (authorities != null && (mAccount == null || mAccount.equals(account))) {
237                for (int j = 0, m = authorities.size(); j < m; j++) {
238                    final String authority = authorities.get(j);
239                    // We could check services here....
240                    int syncState = ContentResolver.getIsSyncable(account, authority);
241                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
242                        Log.d(TAG, "  found authority " + authority + " " + syncState);
243                    }
244                    if (syncState > 0) {
245                        Action action = getAction(account, authority, currentSyncs);
246                        if (action != null) {
247                            actions.add(action);
248                        }
249                    }
250                }
251            }
252        }
253
254        Collections.sort(actions, ADAPTER_COMPARATOR);
255        // Always add a "Sync now | cancel sync" action at the beginning.
256        boolean syncActive = false;
257        List<SyncInfo> syncList = ContentResolver.getCurrentSyncs();
258        for (SyncInfo info : syncList) {
259            if (mAccount.equals(info.account)) {
260                syncActive = true;
261                break;
262            }
263        }
264
265        actions.add(0, new Action.Builder()
266                    .key(!syncActive ? KEY_SYNC_NOW : KEY_CANCEL_SYNC)
267                    .title(getString(!syncActive ? R.string.sync_now : R.string.sync_cancel))
268                    .build());
269        return actions;
270    }
271
272    /**
273     * Gets an action item with the appropriate description / checkmark / drawable.
274     * <p>
275     * Returns null if the provider can't be shown for some reason.
276     */
277    private Action getAction(Account account, String authority, List<SyncInfo> currentSyncs) {
278        final ProviderInfo providerInfo = getPackageManager().resolveContentProvider(authority, 0);
279        if (providerInfo == null) {
280            return null;
281        }
282        CharSequence providerLabel = providerInfo.loadLabel(getPackageManager());
283        if (TextUtils.isEmpty(providerLabel)) {
284            return null;
285        }
286        String description;
287        boolean isSyncing;
288        Date date = new Date();
289        SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority);
290        boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority);
291        boolean authorityIsPending = status == null ? false : status.pending;
292        boolean initialSync = status == null ? false : status.initialize;
293
294        boolean activelySyncing = isSyncing(currentSyncs, account, authority);
295        boolean lastSyncFailed = status != null
296                && status.lastFailureTime != 0
297                && status.getLastFailureMesgAsInt(0)
298                   != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
299        if (!syncEnabled) lastSyncFailed = false;
300        if (lastSyncFailed && !activelySyncing && !authorityIsPending) {
301            mSyncIsFailing = true;
302        }
303        if (Log.isLoggable(TAG, Log.VERBOSE)) {
304            Log.d(TAG, "Update sync status: " + account + " " + authority +
305                    " active = " + activelySyncing + " pend =" +  authorityIsPending);
306        }
307
308        final long successEndTime = (status == null) ? 0 : status.lastSuccessTime;
309        if (!syncEnabled) {
310            description = getString(R.string.sync_disabled);
311        } else if (activelySyncing) {
312            description = getString(R.string.sync_in_progress);
313        } else if (successEndTime != 0) {
314            date.setTime(successEndTime);
315            final String timeString = formatSyncDate(date);
316            description = getString(R.string.last_synced, timeString);
317        } else {
318            description = "";
319        }
320        int syncState = ContentResolver.getIsSyncable(account, authority);
321
322        boolean pending = authorityIsPending && (syncState >= 0) && !initialSync;
323        boolean active = activelySyncing && (syncState >= 0) && !initialSync;
324        boolean activeVisible = pending || active;
325        // TODO: set drawable based on these flags.
326
327        ConnectivityManager connManager =
328                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
329        final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
330        final boolean backgroundDataEnabled = connManager.getBackgroundDataSetting();
331        final boolean oneTimeSyncMode = !masterSyncAutomatically || !backgroundDataEnabled;
332        boolean checked = oneTimeSyncMode || syncEnabled;
333
334        // Store extras in the intent
335        Intent intent = new Intent()
336                .putExtra(EXTRA_ONE_TIME_SYNC, oneTimeSyncMode)
337                .putExtra(EXTRA_ACCOUNT, account);
338
339        if (Log.isLoggable(TAG, Log.VERBOSE)) {
340            Log.d(TAG, "Creating action " + providerLabel.toString() + " " + description);
341        }
342        return new Action.Builder()
343                .key(authority)
344                .title(providerLabel.toString())
345                .description(description)
346                .checked(checked)
347                .intent(intent)
348                .build();
349    }
350
351    private void startSyncForEnabledProviders() {
352        requestOrCancelSyncForEnabledProviders(true /* start them */);
353    }
354
355    private void cancelSyncForEnabledProviders() {
356        requestOrCancelSyncForEnabledProviders(false /* cancel them */);
357    }
358
359    private void requestOrCancelSyncForEnabledProviders(boolean startSync) {
360        // sync everything that the user has enabled
361        int count = mActionFragment.getAdapter().getCount();
362        for (int i = 0; i < count; i++) {
363            Action action = (Action) mActionFragment.getAdapter().getItem(i);
364            if (action.getIntent() == null) {
365                continue;
366            }
367            if (!action.isChecked()) {
368                continue;
369            }
370            Account account = action.getIntent().getParcelableExtra(EXTRA_ACCOUNT);
371            requestOrCancelSync(account, action.getKey(), startSync);
372        }
373        // plus whatever the system needs to sync, e.g., invisible sync adapters
374        if (mAccount != null) {
375            // Make a copy of these in case we update while calling this.
376            List<SyncAdapterType> invisibleAdapters = new ArrayList<SyncAdapterType>(
377                    mInvisibleAdapters);
378            int size = invisibleAdapters.size();
379            for (int index = 0; index < size; ++index) {
380                SyncAdapterType syncAdapter = invisibleAdapters.get(index);
381                // invisible sync adapters' account type should be same as current account type
382                if (syncAdapter.accountType.equals(mAccount.type)) {
383                    requestOrCancelSync(mAccount, syncAdapter.authority, startSync);
384                }
385            }
386        }
387    }
388
389    private void requestOrCancelSync(Account account, String authority, boolean sync) {
390        if (sync) {
391            Bundle extras = new Bundle();
392            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
393            ContentResolver.requestSync(account, authority, extras);
394        } else {
395            ContentResolver.cancelSync(account, authority);
396        }
397    }
398
399    private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) {
400        for (SyncInfo syncInfo : currentSyncs) {
401            if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
402                return true;
403            }
404        }
405        return false;
406    }
407
408    protected String formatSyncDate(Date date) {
409        return DateUtils.formatDateTime(this, date.getTime(),
410                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
411    }
412
413    private class LoadActionsTask extends AsyncTask<Account[], Void, ArrayList<Action>> {
414
415        @Override
416        protected ArrayList<Action> doInBackground(Account[]... params) {
417            return getActions(params[0]);
418        }
419
420        @Override
421        protected void onPostExecute(ArrayList<Action> result) {
422            // Set the icon based on whether sync is failing.
423            ContentFragment contentFragment = ((ContentFragment) getContentFragment());
424            if (contentFragment != null) {
425                contentFragment.setIcon(mSyncIsFailing ? R.drawable.ic_settings_sync_error :
426                        R.drawable.ic_settings_sync);
427                contentFragment.setDescriptionText(mSyncIsFailing ?
428                        getString(R.string.sync_is_failing) : "");
429            }
430            ((ActionAdapter) mActionFragment.getAdapter()).setActions(result);
431        }
432    }
433
434    private static final Comparator<Action> ADAPTER_COMPARATOR = new Comparator<Action>() {
435
436        @Override
437        public int compare(Action lhs, Action rhs) {
438            if (lhs == null && rhs == null) {
439                return 0;
440            }
441            if (lhs != null && rhs == null) {
442                return 1;
443            }
444            if (rhs != null && lhs == null) {
445                return -1;
446            }
447            return lhs.getTitle().compareTo(rhs.getTitle());
448        }
449    };
450}
451