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.OnAccountsUpdateListener;
22import android.app.ActionBar;
23import android.app.Activity;
24import android.content.ContentResolver;
25import android.content.Intent;
26import android.content.SyncAdapterType;
27import android.content.SyncInfo;
28import android.content.SyncStatusInfo;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.graphics.drawable.Drawable;
32import android.os.Bundle;
33import android.preference.Preference;
34import android.preference.PreferenceActivity;
35import android.preference.PreferenceScreen;
36import android.util.Log;
37import android.view.LayoutInflater;
38import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
41import android.view.View;
42import android.view.ViewGroup;
43import android.widget.ListView;
44import android.widget.TextView;
45
46import com.android.settings.AccountPreference;
47import com.android.settings.R;
48import com.android.settings.Utils;
49import com.android.settings.location.LocationSettings;
50
51import java.util.ArrayList;
52import java.util.Date;
53import java.util.HashSet;
54
55/** Manages settings for Google Account. */
56public class ManageAccountsSettings extends AccountPreferenceBase
57        implements OnAccountsUpdateListener {
58    private static final String ACCOUNT_KEY = "account"; // to pass to auth settings
59    public static final String KEY_ACCOUNT_TYPE = "account_type";
60    public static final String KEY_ACCOUNT_LABEL = "account_label";
61
62    // Action name for the broadcast intent when the Google account preferences page is launching
63    // the location settings.
64    private static final String LAUNCHING_LOCATION_SETTINGS =
65            "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS";
66
67    private static final int MENU_SYNC_NOW_ID = Menu.FIRST;
68    private static final int MENU_SYNC_CANCEL_ID    = Menu.FIRST + 1;
69
70    private static final int REQUEST_SHOW_SYNC_SETTINGS = 1;
71
72    private String[] mAuthorities;
73    private TextView mErrorInfoView;
74
75    // If an account type is set, then show only accounts of that type
76    private String mAccountType;
77    // Temporary hack, to deal with backward compatibility
78    private Account mFirstAccount;
79
80    @Override
81    public void onCreate(Bundle icicle) {
82        super.onCreate(icicle);
83
84        Bundle args = getArguments();
85        if (args != null && args.containsKey(KEY_ACCOUNT_TYPE)) {
86            mAccountType = args.getString(KEY_ACCOUNT_TYPE);
87        }
88        addPreferencesFromResource(R.xml.manage_accounts_settings);
89        setHasOptionsMenu(true);
90    }
91
92    @Override
93    public void onStart() {
94        super.onStart();
95        Activity activity = getActivity();
96        AccountManager.get(activity).addOnAccountsUpdatedListener(this, null, true);
97    }
98
99    @Override
100    public View onCreateView(LayoutInflater inflater, ViewGroup container,
101            Bundle savedInstanceState) {
102        final View view = inflater.inflate(R.layout.manage_accounts_screen, container, false);
103        final ListView list = (ListView) view.findViewById(android.R.id.list);
104        Utils.prepareCustomPreferencesList(container, view, list, false);
105        return view;
106    }
107
108    @Override
109    public void onActivityCreated(Bundle savedInstanceState) {
110        super.onActivityCreated(savedInstanceState);
111
112        final Activity activity = getActivity();
113        final View view = getView();
114
115        mErrorInfoView = (TextView)view.findViewById(R.id.sync_settings_error_info);
116        mErrorInfoView.setVisibility(View.GONE);
117
118        mAuthorities = activity.getIntent().getStringArrayExtra(AUTHORITIES_FILTER_KEY);
119
120        Bundle args = getArguments();
121        if (args != null && args.containsKey(KEY_ACCOUNT_LABEL)) {
122            getActivity().setTitle(args.getString(KEY_ACCOUNT_LABEL));
123        }
124        updateAuthDescriptions();
125    }
126
127    @Override
128    public void onStop() {
129        super.onStop();
130        final Activity activity = getActivity();
131        AccountManager.get(activity).removeOnAccountsUpdatedListener(this);
132        activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM);
133        activity.getActionBar().setCustomView(null);
134    }
135
136    @Override
137    public boolean onPreferenceTreeClick(PreferenceScreen preferences, Preference preference) {
138        if (preference instanceof AccountPreference) {
139            startAccountSettings((AccountPreference) preference);
140        } else {
141            return false;
142        }
143        return true;
144    }
145
146    private void startAccountSettings(AccountPreference acctPref) {
147        Bundle args = new Bundle();
148        args.putParcelable(AccountSyncSettings.ACCOUNT_KEY, acctPref.getAccount());
149        ((PreferenceActivity) getActivity()).startPreferencePanel(
150                AccountSyncSettings.class.getCanonicalName(), args,
151                R.string.account_sync_settings_title, acctPref.getAccount().name,
152                this, REQUEST_SHOW_SYNC_SETTINGS);
153    }
154
155    @Override
156    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
157        MenuItem syncNow = menu.add(0, MENU_SYNC_NOW_ID, 0,
158                getString(R.string.sync_menu_sync_now))
159                .setIcon(R.drawable.ic_menu_refresh_holo_dark);
160        MenuItem syncCancel = menu.add(0, MENU_SYNC_CANCEL_ID, 0,
161                getString(R.string.sync_menu_sync_cancel))
162                .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel);
163        super.onCreateOptionsMenu(menu, inflater);
164    }
165
166    @Override
167    public void onPrepareOptionsMenu(Menu menu) {
168        super.onPrepareOptionsMenu(menu);
169        boolean syncActive = ContentResolver.getCurrentSync() != null;
170        menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive && mFirstAccount != null);
171        menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive && mFirstAccount != null);
172    }
173
174    @Override
175    public boolean onOptionsItemSelected(MenuItem item) {
176        switch (item.getItemId()) {
177        case MENU_SYNC_NOW_ID:
178            requestOrCancelSyncForAccounts(true);
179            return true;
180        case MENU_SYNC_CANCEL_ID:
181            requestOrCancelSyncForAccounts(false);
182            return true;
183        }
184        return super.onOptionsItemSelected(item);
185    }
186
187    private void requestOrCancelSyncForAccounts(boolean sync) {
188        SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes();
189        Bundle extras = new Bundle();
190        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
191        int count = getPreferenceScreen().getPreferenceCount();
192        // For each account
193        for (int i = 0; i < count; i++) {
194            Preference pref = getPreferenceScreen().getPreference(i);
195            if (pref instanceof AccountPreference) {
196                Account account = ((AccountPreference) pref).getAccount();
197                // For all available sync authorities, sync those that are enabled for the account
198                for (int j = 0; j < syncAdapters.length; j++) {
199                    SyncAdapterType sa = syncAdapters[j];
200                    if (syncAdapters[j].accountType.equals(mAccountType)
201                            && ContentResolver.getSyncAutomatically(account, sa.authority)) {
202                        if (sync) {
203                            ContentResolver.requestSync(account, sa.authority, extras);
204                        } else {
205                            ContentResolver.cancelSync(account, sa.authority);
206                        }
207                    }
208                }
209            }
210        }
211    }
212
213    @Override
214    protected void onSyncStateUpdated() {
215        // Catch any delayed delivery of update messages
216        if (getActivity() == null) return;
217
218        // iterate over all the preferences, setting the state properly for each
219        SyncInfo currentSync = ContentResolver.getCurrentSync();
220
221        boolean anySyncFailed = false; // true if sync on any account failed
222        Date date = new Date();
223
224        // only track userfacing sync adapters when deciding if account is synced or not
225        final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes();
226        HashSet<String> userFacing = new HashSet<String>();
227        for (int k = 0, n = syncAdapters.length; k < n; k++) {
228            final SyncAdapterType sa = syncAdapters[k];
229            if (sa.isUserVisible()) {
230                userFacing.add(sa.authority);
231            }
232        }
233        for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) {
234            Preference pref = getPreferenceScreen().getPreference(i);
235            if (! (pref instanceof AccountPreference)) {
236                continue;
237            }
238
239            AccountPreference accountPref = (AccountPreference) pref;
240            Account account = accountPref.getAccount();
241            int syncCount = 0;
242            long lastSuccessTime = 0;
243            boolean syncIsFailing = false;
244            final ArrayList<String> authorities = accountPref.getAuthorities();
245            boolean syncingNow = false;
246            if (authorities != null) {
247                for (String authority : authorities) {
248                    SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority);
249                    boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority)
250                            && ContentResolver.getMasterSyncAutomatically()
251                            && (ContentResolver.getIsSyncable(account, authority) > 0);
252                    boolean authorityIsPending = ContentResolver.isSyncPending(account, authority);
253                    boolean activelySyncing = currentSync != null
254                            && currentSync.authority.equals(authority)
255                            && new Account(currentSync.account.name, currentSync.account.type)
256                                    .equals(account);
257                    boolean lastSyncFailed = status != null
258                            && syncEnabled
259                            && status.lastFailureTime != 0
260                            && status.getLastFailureMesgAsInt(0)
261                               != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
262                    if (lastSyncFailed && !activelySyncing && !authorityIsPending) {
263                        syncIsFailing = true;
264                        anySyncFailed = true;
265                    }
266                    syncingNow |= activelySyncing;
267                    if (status != null && lastSuccessTime < status.lastSuccessTime) {
268                        lastSuccessTime = status.lastSuccessTime;
269                    }
270                    syncCount += syncEnabled && userFacing.contains(authority) ? 1 : 0;
271                }
272            } else {
273                if (Log.isLoggable(TAG, Log.VERBOSE)) {
274                    Log.v(TAG, "no syncadapters found for " + account);
275                }
276            }
277            if (syncIsFailing) {
278                accountPref.setSyncStatus(AccountPreference.SYNC_ERROR, true);
279            } else if (syncCount == 0) {
280                accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
281            } else if (syncCount > 0) {
282                if (syncingNow) {
283                    accountPref.setSyncStatus(AccountPreference.SYNC_IN_PROGRESS, true);
284                } else {
285                    accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, true);
286                    if (lastSuccessTime > 0) {
287                        accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, false);
288                        date.setTime(lastSuccessTime);
289                        final String timeString = formatSyncDate(date);
290                        accountPref.setSummary(getResources().getString(
291                                R.string.last_synced, timeString));
292                    }
293                }
294            } else {
295                accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
296            }
297        }
298
299        mErrorInfoView.setVisibility(anySyncFailed ? View.VISIBLE : View.GONE);
300    }
301
302    @Override
303    public void onAccountsUpdated(Account[] accounts) {
304        if (getActivity() == null) return;
305        getPreferenceScreen().removeAll();
306        mFirstAccount = null;
307        addPreferencesFromResource(R.xml.manage_accounts_settings);
308        for (int i = 0, n = accounts.length; i < n; i++) {
309            final Account account = accounts[i];
310            // If an account type is specified for this screen, skip other types
311            if (mAccountType != null && !account.type.equals(mAccountType)) continue;
312            final ArrayList<String> auths = getAuthoritiesForAccountType(account.type);
313
314            boolean showAccount = true;
315            if (mAuthorities != null && auths != null) {
316                showAccount = false;
317                for (String requestedAuthority : mAuthorities) {
318                    if (auths.contains(requestedAuthority)) {
319                        showAccount = true;
320                        break;
321                    }
322                }
323            }
324
325            if (showAccount) {
326                final Drawable icon = getDrawableForType(account.type);
327                final AccountPreference preference =
328                        new AccountPreference(getActivity(), account, icon, auths, false);
329                getPreferenceScreen().addPreference(preference);
330                if (mFirstAccount == null) {
331                    mFirstAccount = account;
332                    getActivity().invalidateOptionsMenu();
333                }
334            }
335        }
336        if (mAccountType != null && mFirstAccount != null) {
337            addAuthenticatorSettings();
338        } else {
339            // There's no account, reset to top-level of settings
340            Intent settingsTop = new Intent(android.provider.Settings.ACTION_SETTINGS);
341            settingsTop.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
342            getActivity().startActivity(settingsTop);
343        }
344        onSyncStateUpdated();
345    }
346
347    private void addAuthenticatorSettings() {
348        PreferenceScreen prefs = addPreferencesForType(mAccountType, getPreferenceScreen());
349        if (prefs != null) {
350            updatePreferenceIntents(prefs);
351        }
352    }
353
354    /** Listens to a preference click event and starts a fragment */
355    private class FragmentStarter
356            implements Preference.OnPreferenceClickListener {
357        private final String mClass;
358        private final int mTitleRes;
359
360        /**
361         * @param className the class name of the fragment to be started.
362         * @param title the title resource id of the started preference panel.
363         */
364        public FragmentStarter(String className, int title) {
365            mClass = className;
366            mTitleRes = title;
367        }
368
369        @Override
370        public boolean onPreferenceClick(Preference preference) {
371            ((PreferenceActivity) getActivity()).startPreferencePanel(
372                    mClass, null, mTitleRes, null, null, 0);
373            // Hack: announce that the Google account preferences page is launching the location
374            // settings
375            if (mClass.equals(LocationSettings.class.getName())) {
376                Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS);
377                getActivity().sendBroadcast(
378                        intent, android.Manifest.permission.WRITE_SECURE_SETTINGS);
379            }
380            return true;
381        }
382    }
383
384    /**
385     * Filters through the preference list provided by GoogleLoginService.
386     *
387     * This method removes all the invalid intent from the list, adds account name as extra into the
388     * intent, and hack the location settings to start it as a fragment.
389     */
390    private void updatePreferenceIntents(PreferenceScreen prefs) {
391        PackageManager pm = getActivity().getPackageManager();
392        for (int i = 0; i < prefs.getPreferenceCount();) {
393            Preference pref = prefs.getPreference(i);
394            Intent intent = pref.getIntent();
395            if (intent != null) {
396                // Hack. Launch "Location" as fragment instead of as activity.
397                //
398                // When "Location" is launched as activity via Intent, there's no "Up" button at the
399                // top left, and if there's another running instance of "Location" activity, the
400                // back stack would usually point to some other place so the user won't be able to
401                // go back to the previous page by "back" key. Using fragment is a much easier
402                // solution to those problems.
403                //
404                // If we set Intent to null and assign a fragment to the PreferenceScreen item here,
405                // in order to make it work as expected, we still need to modify the container
406                // PreferenceActivity, override onPreferenceStartFragment() and call
407                // startPreferencePanel() there. In order to inject the title string there, more
408                // dirty further hack is still needed. It's much easier and cleaner to listen to
409                // preference click event here directly.
410                if (intent.getAction().equals(
411                        android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) {
412                    // The OnPreferenceClickListener overrides the click event completely. No intent
413                    // will get fired.
414                    pref.setOnPreferenceClickListener(new FragmentStarter(
415                            LocationSettings.class.getName(),
416                            R.string.location_settings_title));
417                } else {
418                    ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
419                    if (ri == null) {
420                        prefs.removePreference(pref);
421                        continue;
422                    } else {
423                        intent.putExtra(ACCOUNT_KEY, mFirstAccount);
424                        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
425                    }
426                }
427            }
428            i++;
429        }
430    }
431
432    @Override
433    protected void onAuthDescriptionsUpdated() {
434        // Update account icons for all account preference items
435        for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
436            Preference pref = getPreferenceScreen().getPreference(i);
437            if (pref instanceof AccountPreference) {
438                AccountPreference accPref = (AccountPreference) pref;
439                accPref.setSummary(getLabelForType(accPref.getAccount().type));
440            }
441        }
442    }
443}
444