1/*
2 * Copyright (C) 2008 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.settings.accounts;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.accounts.AccountManagerCallback;
22import android.accounts.AccountManagerFuture;
23import android.accounts.AuthenticatorException;
24import android.accounts.OperationCanceledException;
25import android.app.Activity;
26import android.app.AlertDialog;
27import android.app.Dialog;
28import android.content.ContentResolver;
29import android.content.Context;
30import android.content.DialogInterface;
31import android.content.SyncAdapterType;
32import android.content.SyncInfo;
33import android.content.SyncStatusInfo;
34import android.content.pm.ProviderInfo;
35import android.net.ConnectivityManager;
36import android.os.Bundle;
37import android.os.UserManager;
38import android.preference.Preference;
39import android.preference.PreferenceScreen;
40import android.text.TextUtils;
41import android.util.Log;
42import android.view.LayoutInflater;
43import android.view.Menu;
44import android.view.MenuInflater;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.ViewGroup;
48import android.widget.ImageView;
49import android.widget.ListView;
50import android.widget.TextView;
51
52import com.android.settings.R;
53import com.android.settings.Utils;
54import com.google.android.collect.Lists;
55import com.google.android.collect.Maps;
56
57import java.io.IOException;
58import java.util.ArrayList;
59import java.util.Collections;
60import java.util.Date;
61import java.util.HashMap;
62import java.util.List;
63
64public class AccountSyncSettings extends AccountPreferenceBase {
65
66    public static final String ACCOUNT_KEY = "account";
67    private static final int MENU_SYNC_NOW_ID       = Menu.FIRST;
68    private static final int MENU_SYNC_CANCEL_ID    = Menu.FIRST + 1;
69    private static final int MENU_REMOVE_ACCOUNT_ID = Menu.FIRST + 2;
70    private static final int REALLY_REMOVE_DIALOG = 100;
71    private static final int FAILED_REMOVAL_DIALOG = 101;
72    private static final int CANT_DO_ONETIME_SYNC_DIALOG = 102;
73    private TextView mUserId;
74    private TextView mProviderId;
75    private ImageView mProviderIcon;
76    private TextView mErrorInfoView;
77    private Account mAccount;
78    // List of all accounts, updated when accounts are added/removed
79    // We need to re-scan the accounts on sync events, in case sync state changes.
80    private Account[] mAccounts;
81    private ArrayList<SyncStateCheckBoxPreference> mCheckBoxes =
82                new ArrayList<SyncStateCheckBoxPreference>();
83    private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList();
84
85    @Override
86    public Dialog onCreateDialog(final int id) {
87        Dialog dialog = null;
88        if (id == REALLY_REMOVE_DIALOG) {
89            dialog = new AlertDialog.Builder(getActivity())
90                .setTitle(R.string.really_remove_account_title)
91                .setMessage(R.string.really_remove_account_message)
92                .setNegativeButton(android.R.string.cancel, null)
93                .setPositiveButton(R.string.remove_account_label,
94                        new DialogInterface.OnClickListener() {
95                    @Override
96                    public void onClick(DialogInterface dialog, int which) {
97                        AccountManager.get(AccountSyncSettings.this.getActivity())
98                                .removeAccount(mAccount,
99                                new AccountManagerCallback<Boolean>() {
100                            @Override
101                            public void run(AccountManagerFuture<Boolean> future) {
102                                // If already out of this screen, don't proceed.
103                                if (!AccountSyncSettings.this.isResumed()) {
104                                    return;
105                                }
106                                boolean failed = true;
107                                try {
108                                    if (future.getResult() == true) {
109                                        failed = false;
110                                    }
111                                } catch (OperationCanceledException e) {
112                                    // handled below
113                                } catch (IOException e) {
114                                    // handled below
115                                } catch (AuthenticatorException e) {
116                                    // handled below
117                                }
118                                if (failed && getActivity() != null &&
119                                        !getActivity().isFinishing()) {
120                                    showDialog(FAILED_REMOVAL_DIALOG);
121                                } else {
122                                    finish();
123                                }
124                            }
125                        }, null);
126                    }
127                })
128                .create();
129        } else if (id == FAILED_REMOVAL_DIALOG) {
130            dialog = new AlertDialog.Builder(getActivity())
131                .setTitle(R.string.really_remove_account_title)
132                .setPositiveButton(android.R.string.ok, null)
133                .setMessage(R.string.remove_account_failed)
134                .create();
135        } else if (id == CANT_DO_ONETIME_SYNC_DIALOG) {
136            dialog = new AlertDialog.Builder(getActivity())
137                .setTitle(R.string.cant_sync_dialog_title)
138                .setMessage(R.string.cant_sync_dialog_message)
139                .setPositiveButton(android.R.string.ok, null)
140                .create();
141        }
142        return dialog;
143    }
144
145    @Override
146    public void onCreate(Bundle icicle) {
147        super.onCreate(icicle);
148
149        setHasOptionsMenu(true);
150    }
151
152    @Override
153    public View onCreateView(LayoutInflater inflater, ViewGroup container,
154            Bundle savedInstanceState) {
155        final View view = inflater.inflate(R.layout.account_sync_screen, container, false);
156
157        final ListView list = (ListView) view.findViewById(android.R.id.list);
158        Utils.prepareCustomPreferencesList(container, view, list, false);
159
160        initializeUi(view);
161
162        return view;
163    }
164
165    protected void initializeUi(final View rootView) {
166        addPreferencesFromResource(R.xml.account_sync_settings);
167
168        mErrorInfoView = (TextView) rootView.findViewById(R.id.sync_settings_error_info);
169        mErrorInfoView.setVisibility(View.GONE);
170
171        mUserId = (TextView) rootView.findViewById(R.id.user_id);
172        mProviderId = (TextView) rootView.findViewById(R.id.provider_id);
173        mProviderIcon = (ImageView) rootView.findViewById(R.id.provider_icon);
174    }
175
176    @Override
177    public void onActivityCreated(Bundle savedInstanceState) {
178        super.onActivityCreated(savedInstanceState);
179
180        Bundle arguments = getArguments();
181        if (arguments == null) {
182            Log.e(TAG, "No arguments provided when starting intent. ACCOUNT_KEY needed.");
183            return;
184        }
185
186        mAccount = (Account) arguments.getParcelable(ACCOUNT_KEY);
187        if (mAccount != null) {
188            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Got account: " + mAccount);
189            mUserId.setText(mAccount.name);
190            mProviderId.setText(mAccount.type);
191        }
192    }
193
194    @Override
195    public void onResume() {
196        final Activity activity = getActivity();
197        AccountManager.get(activity).addOnAccountsUpdatedListener(this, null, false);
198        updateAuthDescriptions();
199        onAccountsUpdated(AccountManager.get(activity).getAccounts());
200
201        super.onResume();
202    }
203
204    @Override
205    public void onPause() {
206        super.onPause();
207        AccountManager.get(getActivity()).removeOnAccountsUpdatedListener(this);
208    }
209
210    private void addSyncStateCheckBox(Account account, String authority) {
211        SyncStateCheckBoxPreference item =
212                new SyncStateCheckBoxPreference(getActivity(), account, authority);
213        item.setPersistent(false);
214        final ProviderInfo providerInfo = getPackageManager().resolveContentProvider(authority, 0);
215        if (providerInfo == null) {
216            return;
217        }
218        CharSequence providerLabel = providerInfo.loadLabel(getPackageManager());
219        if (TextUtils.isEmpty(providerLabel)) {
220            Log.e(TAG, "Provider needs a label for authority '" + authority + "'");
221            return;
222        }
223        String title = getString(R.string.sync_item_title, providerLabel);
224        item.setTitle(title);
225        item.setKey(authority);
226        mCheckBoxes.add(item);
227    }
228
229    @Override
230    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
231
232        MenuItem syncNow = menu.add(0, MENU_SYNC_NOW_ID, 0,
233                getString(R.string.sync_menu_sync_now))
234                .setIcon(R.drawable.ic_menu_refresh_holo_dark);
235        MenuItem syncCancel = menu.add(0, MENU_SYNC_CANCEL_ID, 0,
236                getString(R.string.sync_menu_sync_cancel))
237                .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel);
238
239        final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
240        if (!um.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) {
241            MenuItem removeAccount = menu.add(0, MENU_REMOVE_ACCOUNT_ID, 0,
242                    getString(R.string.remove_account_label))
243                    .setIcon(R.drawable.ic_menu_delete_holo_dark);
244            removeAccount.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER |
245                    MenuItem.SHOW_AS_ACTION_WITH_TEXT);
246        }
247        syncNow.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER |
248                MenuItem.SHOW_AS_ACTION_WITH_TEXT);
249        syncCancel.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER |
250                MenuItem.SHOW_AS_ACTION_WITH_TEXT);
251
252        super.onCreateOptionsMenu(menu, inflater);
253    }
254
255    @Override
256    public void onPrepareOptionsMenu(Menu menu) {
257        super.onPrepareOptionsMenu(menu);
258        boolean syncActive = ContentResolver.getCurrentSync() != null;
259        menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive);
260        menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive);
261    }
262
263    @Override
264    public boolean onOptionsItemSelected(MenuItem item) {
265        switch (item.getItemId()) {
266            case MENU_SYNC_NOW_ID:
267                startSyncForEnabledProviders();
268                return true;
269            case MENU_SYNC_CANCEL_ID:
270                cancelSyncForEnabledProviders();
271                return true;
272            case MENU_REMOVE_ACCOUNT_ID:
273                showDialog(REALLY_REMOVE_DIALOG);
274                return true;
275        }
276        return super.onOptionsItemSelected(item);
277    }
278
279    @Override
280    public boolean onPreferenceTreeClick(PreferenceScreen preferences, Preference preference) {
281        if (preference instanceof SyncStateCheckBoxPreference) {
282            SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) preference;
283            String authority = syncPref.getAuthority();
284            Account account = syncPref.getAccount();
285            boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority);
286            if (syncPref.isOneTimeSyncMode()) {
287                requestOrCancelSync(account, authority, true);
288            } else {
289                boolean syncOn = syncPref.isChecked();
290                boolean oldSyncState = syncAutomatically;
291                if (syncOn != oldSyncState) {
292                    // if we're enabling sync, this will request a sync as well
293                    ContentResolver.setSyncAutomatically(account, authority, syncOn);
294                    // if the master sync switch is off, the request above will
295                    // get dropped.  when the user clicks on this toggle,
296                    // we want to force the sync, however.
297                    if (!ContentResolver.getMasterSyncAutomatically() || !syncOn) {
298                        requestOrCancelSync(account, authority, syncOn);
299                    }
300                }
301            }
302            return true;
303        } else {
304            return super.onPreferenceTreeClick(preferences, preference);
305        }
306    }
307
308    private void startSyncForEnabledProviders() {
309        requestOrCancelSyncForEnabledProviders(true /* start them */);
310        getActivity().invalidateOptionsMenu();
311    }
312
313    private void cancelSyncForEnabledProviders() {
314        requestOrCancelSyncForEnabledProviders(false /* cancel them */);
315        getActivity().invalidateOptionsMenu();
316    }
317
318    private void requestOrCancelSyncForEnabledProviders(boolean startSync) {
319        // sync everything that the user has enabled
320        int count = getPreferenceScreen().getPreferenceCount();
321        for (int i = 0; i < count; i++) {
322            Preference pref = getPreferenceScreen().getPreference(i);
323            if (! (pref instanceof SyncStateCheckBoxPreference)) {
324                continue;
325            }
326            SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) pref;
327            if (!syncPref.isChecked()) {
328                continue;
329            }
330            requestOrCancelSync(syncPref.getAccount(), syncPref.getAuthority(), startSync);
331        }
332        // plus whatever the system needs to sync, e.g., invisible sync adapters
333        if (mAccount != null) {
334            for (SyncAdapterType syncAdapter : mInvisibleAdapters) {
335                // invisible sync adapters' account type should be same as current account type
336                if (syncAdapter.accountType.equals(mAccount.type)) {
337                    requestOrCancelSync(mAccount, syncAdapter.authority, startSync);
338                }
339            }
340        }
341    }
342
343    private void requestOrCancelSync(Account account, String authority, boolean flag) {
344        if (flag) {
345            Bundle extras = new Bundle();
346            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
347            ContentResolver.requestSync(account, authority, extras);
348        } else {
349            ContentResolver.cancelSync(account, authority);
350        }
351    }
352
353    private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) {
354        for (SyncInfo syncInfo : currentSyncs) {
355            if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
356                return true;
357            }
358        }
359        return false;
360    }
361
362    @Override
363    protected void onSyncStateUpdated() {
364        if (!isResumed()) return;
365        setFeedsState();
366    }
367
368    private void setFeedsState() {
369        // iterate over all the preferences, setting the state properly for each
370        Date date = new Date();
371        List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncs();
372        boolean syncIsFailing = false;
373
374        // Refresh the sync status checkboxes - some syncs may have become active.
375        updateAccountCheckboxes(mAccounts);
376
377        for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) {
378            Preference pref = getPreferenceScreen().getPreference(i);
379            if (! (pref instanceof SyncStateCheckBoxPreference)) {
380                continue;
381            }
382            SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) pref;
383
384            String authority = syncPref.getAuthority();
385            Account account = syncPref.getAccount();
386
387            SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority);
388            boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority);
389            boolean authorityIsPending = status == null ? false : status.pending;
390            boolean initialSync = status == null ? false : status.initialize;
391
392            boolean activelySyncing = isSyncing(currentSyncs, account, authority);
393            boolean lastSyncFailed = status != null
394                    && status.lastFailureTime != 0
395                    && status.getLastFailureMesgAsInt(0)
396                       != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
397            if (!syncEnabled) lastSyncFailed = false;
398            if (lastSyncFailed && !activelySyncing && !authorityIsPending) {
399                syncIsFailing = true;
400            }
401            if (Log.isLoggable(TAG, Log.VERBOSE)) {
402                Log.d(TAG, "Update sync status: " + account + " " + authority +
403                        " active = " + activelySyncing + " pend =" +  authorityIsPending);
404            }
405
406            final long successEndTime = (status == null) ? 0 : status.lastSuccessTime;
407            if (!syncEnabled) {
408                syncPref.setSummary(R.string.sync_disabled);
409            } else if (activelySyncing) {
410                syncPref.setSummary(R.string.sync_in_progress);
411            } else if (successEndTime != 0) {
412                date.setTime(successEndTime);
413                final String timeString = formatSyncDate(date);
414                syncPref.setSummary(getResources().getString(R.string.last_synced, timeString));
415            } else {
416                syncPref.setSummary("");
417            }
418            int syncState = ContentResolver.getIsSyncable(account, authority);
419
420            syncPref.setActive(activelySyncing && (syncState >= 0) &&
421                    !initialSync);
422            syncPref.setPending(authorityIsPending && (syncState >= 0) &&
423                    !initialSync);
424
425            syncPref.setFailed(lastSyncFailed);
426            ConnectivityManager connManager =
427                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
428            final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
429            final boolean backgroundDataEnabled = connManager.getBackgroundDataSetting();
430            final boolean oneTimeSyncMode = !masterSyncAutomatically || !backgroundDataEnabled;
431            syncPref.setOneTimeSyncMode(oneTimeSyncMode);
432            syncPref.setChecked(oneTimeSyncMode || syncEnabled);
433        }
434        mErrorInfoView.setVisibility(syncIsFailing ? View.VISIBLE : View.GONE);
435        getActivity().invalidateOptionsMenu();
436    }
437
438    @Override
439    public void onAccountsUpdated(Account[] accounts) {
440        super.onAccountsUpdated(accounts);
441        mAccounts = accounts;
442        updateAccountCheckboxes(accounts);
443        onSyncStateUpdated();
444    }
445
446    private void updateAccountCheckboxes(Account[] accounts) {
447        mInvisibleAdapters.clear();
448
449        SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes();
450        HashMap<String, ArrayList<String>> accountTypeToAuthorities =
451            Maps.newHashMap();
452        for (int i = 0, n = syncAdapters.length; i < n; i++) {
453            final SyncAdapterType sa = syncAdapters[i];
454            if (sa.isUserVisible()) {
455                ArrayList<String> authorities = accountTypeToAuthorities.get(sa.accountType);
456                if (authorities == null) {
457                    authorities = new ArrayList<String>();
458                    accountTypeToAuthorities.put(sa.accountType, authorities);
459                }
460                if (Log.isLoggable(TAG, Log.VERBOSE)) {
461                    Log.d(TAG, "onAccountUpdated: added authority " + sa.authority
462                            + " to accountType " + sa.accountType);
463                }
464                authorities.add(sa.authority);
465            } else {
466                // keep track of invisible sync adapters, so sync now forces
467                // them to sync as well.
468                mInvisibleAdapters.add(sa);
469            }
470        }
471
472        for (int i = 0, n = mCheckBoxes.size(); i < n; i++) {
473            getPreferenceScreen().removePreference(mCheckBoxes.get(i));
474        }
475        mCheckBoxes.clear();
476
477        for (int i = 0, n = accounts.length; i < n; i++) {
478            final Account account = accounts[i];
479            if (Log.isLoggable(TAG, Log.VERBOSE)) {
480                Log.d(TAG, "looking for sync adapters that match account " + account);
481            }
482            final ArrayList<String> authorities = accountTypeToAuthorities.get(account.type);
483            if (authorities != null && (mAccount == null || mAccount.equals(account))) {
484                for (int j = 0, m = authorities.size(); j < m; j++) {
485                    final String authority = authorities.get(j);
486                    // We could check services here....
487                    int syncState = ContentResolver.getIsSyncable(account, authority);
488                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
489                        Log.d(TAG, "  found authority " + authority + " " + syncState);
490                    }
491                    if (syncState > 0) {
492                        addSyncStateCheckBox(account, authority);
493                    }
494                }
495            }
496        }
497
498        Collections.sort(mCheckBoxes);
499        for (int i = 0, n = mCheckBoxes.size(); i < n; i++) {
500            getPreferenceScreen().addPreference(mCheckBoxes.get(i));
501        }
502    }
503
504    /**
505     * Updates the titlebar with an icon for the provider type.
506     */
507    @Override
508    protected void onAuthDescriptionsUpdated() {
509        super.onAuthDescriptionsUpdated();
510        getPreferenceScreen().removeAll();
511        if (mAccount != null) {
512            mProviderIcon.setImageDrawable(getDrawableForType(mAccount.type));
513            mProviderId.setText(getLabelForType(mAccount.type));
514        }
515        addPreferencesFromResource(R.xml.account_sync_settings);
516    }
517
518    @Override
519    protected int getHelpResource() {
520        return R.string.help_url_accounts;
521    }
522}
523