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