1/*
2 * Copyright (C) 2015 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 android.accounts.Account;
20import android.accounts.AccountManager;
21import android.app.Activity;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.SyncAdapterType;
26import android.content.SyncInfo;
27import android.content.SyncStatusInfo;
28import android.content.SyncStatusObserver;
29import android.content.pm.PackageManager;
30import android.content.pm.ProviderInfo;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.UserHandle;
34import android.support.v17.preference.LeanbackPreferenceFragment;
35import android.support.v7.preference.Preference;
36import android.support.v7.preference.PreferenceGroup;
37import android.text.TextUtils;
38import android.text.format.DateUtils;
39import android.util.Log;
40
41import com.android.settingslib.accounts.AuthenticatorHelper;
42import com.android.tv.settings.R;
43
44import com.google.android.collect.Lists;
45
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.List;
49
50public class AccountSyncFragment extends LeanbackPreferenceFragment implements
51        AuthenticatorHelper.OnAccountsUpdateListener {
52    private static final String TAG = "AccountSyncFragment";
53
54    private static final String ARG_ACCOUNT = "account";
55    private static final String KEY_REMOVE_ACCOUNT = "remove_account";
56    private static final String KEY_SYNC_NOW = "sync_now";
57    private static final String KEY_SYNC_ADAPTERS = "sync_adapters";
58
59    private Object mStatusChangeListenerHandle;
60    private UserHandle mUserHandle;
61    private Account mAccount;
62    private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList();
63
64    private PreferenceGroup mSyncCategory;
65
66    private final Handler mHandler = new Handler();
67    private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
68        public void onStatusChanged(int which) {
69            mHandler.post(new Runnable() {
70                public void run() {
71                    if (isResumed()) {
72                        onSyncStateUpdated();
73                    }
74                }
75            });
76        }
77    };
78    private AuthenticatorHelper mAuthenticatorHelper;
79
80    public static AccountSyncFragment newInstance(Account account) {
81        final Bundle b = new Bundle(1);
82        prepareArgs(b, account);
83        final AccountSyncFragment f = new AccountSyncFragment();
84        f.setArguments(b);
85        return f;
86    }
87
88    public static void prepareArgs(Bundle b, Account account) {
89        b.putParcelable(ARG_ACCOUNT, account);
90    }
91
92    @Override
93    public void onCreate(Bundle savedInstanceState) {
94        mUserHandle = new UserHandle(UserHandle.myUserId());
95        mAccount = getArguments().getParcelable(ARG_ACCOUNT);
96        mAuthenticatorHelper = new AuthenticatorHelper(getActivity(), mUserHandle, this);
97
98        super.onCreate(savedInstanceState);
99
100        if (Log.isLoggable(TAG, Log.VERBOSE)) {
101            Log.v(TAG, "Got account: " + mAccount);
102        }
103    }
104
105    @Override
106    public void onStart() {
107        super.onStart();
108        mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
109                ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
110                        | ContentResolver.SYNC_OBSERVER_TYPE_STATUS
111                        | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
112                mSyncStatusObserver);
113        onSyncStateUpdated();
114        mAuthenticatorHelper.listenToAccountUpdates();
115        mAuthenticatorHelper.updateAuthDescriptions(getActivity());
116    }
117
118    @Override
119    public void onResume() {
120        super.onResume();
121        mHandler.post(() -> onAccountsUpdate(mUserHandle));
122    }
123
124    @Override
125    public void onStop() {
126        super.onStop();
127        ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
128        mAuthenticatorHelper.stopListeningToAccountUpdates();
129    }
130
131    @Override
132    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
133        setPreferencesFromResource(R.xml.account_preference, null);
134
135        getPreferenceScreen().setTitle(mAccount.name);
136
137        final Preference removeAccountPref = findPreference(KEY_REMOVE_ACCOUNT);
138        removeAccountPref.setIntent(new Intent(getActivity(), RemoveAccountDialog.class)
139                .putExtra(AccountSyncActivity.EXTRA_ACCOUNT, mAccount.name));
140
141        mSyncCategory = (PreferenceGroup) findPreference(KEY_SYNC_ADAPTERS);
142    }
143
144    @Override
145    public boolean onPreferenceTreeClick(Preference preference) {
146        if (preference instanceof SyncStateSwitchPreference) {
147            SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) preference;
148            String authority = syncPref.getAuthority();
149            Account account = syncPref.getAccount();
150            final int userId = mUserHandle.getIdentifier();
151            if (syncPref.isOneTimeSyncMode()) {
152                requestOrCancelSync(account, authority, true);
153            } else {
154                boolean syncOn = syncPref.isChecked();
155                boolean oldSyncState = ContentResolver.getSyncAutomaticallyAsUser(account,
156                        authority, userId);
157                if (syncOn != oldSyncState) {
158                    // if we're enabling sync, this will request a sync as well
159                    ContentResolver.setSyncAutomaticallyAsUser(account, authority, syncOn, userId);
160                    // if the master sync switch is off, the request above will
161                    // get dropped.  when the user clicks on this toggle,
162                    // we want to force the sync, however.
163                    if (!ContentResolver.getMasterSyncAutomaticallyAsUser(userId) || !syncOn) {
164                        requestOrCancelSync(account, authority, syncOn);
165                    }
166                }
167            }
168            return true;
169        } else if (TextUtils.equals(preference.getKey(), KEY_SYNC_NOW)) {
170            boolean syncActive = !ContentResolver.getCurrentSyncsAsUser(
171                    mUserHandle.getIdentifier()).isEmpty();
172            if (syncActive) {
173                cancelSyncForEnabledProviders();
174            } else {
175                startSyncForEnabledProviders();
176            }
177            return true;
178        } else {
179            return super.onPreferenceTreeClick(preference);
180        }
181    }
182
183    private void startSyncForEnabledProviders() {
184        requestOrCancelSyncForEnabledProviders(true /* start them */);
185        final Activity activity = getActivity();
186        if (activity != null) {
187            activity.invalidateOptionsMenu();
188        }
189    }
190
191    private void cancelSyncForEnabledProviders() {
192        requestOrCancelSyncForEnabledProviders(false /* cancel them */);
193        final Activity activity = getActivity();
194        if (activity != null) {
195            activity.invalidateOptionsMenu();
196        }
197    }
198
199    private void requestOrCancelSyncForEnabledProviders(boolean startSync) {
200        // sync everything that the user has enabled
201        int count = mSyncCategory.getPreferenceCount();
202        for (int i = 0; i < count; i++) {
203            Preference pref = mSyncCategory.getPreference(i);
204            if (! (pref instanceof SyncStateSwitchPreference)) {
205                continue;
206            }
207            SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) pref;
208            if (!syncPref.isChecked()) {
209                continue;
210            }
211            requestOrCancelSync(syncPref.getAccount(), syncPref.getAuthority(), startSync);
212        }
213        // plus whatever the system needs to sync, e.g., invisible sync adapters
214        if (mAccount != null) {
215            for (SyncAdapterType syncAdapter : mInvisibleAdapters) {
216                requestOrCancelSync(mAccount, syncAdapter.authority, startSync);
217            }
218        }
219    }
220
221    private void requestOrCancelSync(Account account, String authority, boolean flag) {
222        if (flag) {
223            Bundle extras = new Bundle();
224            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
225            ContentResolver.requestSyncAsUser(account, authority, mUserHandle.getIdentifier(),
226                    extras);
227        } else {
228            ContentResolver.cancelSyncAsUser(account, authority, mUserHandle.getIdentifier());
229        }
230    }
231
232    private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) {
233        for (SyncInfo syncInfo : currentSyncs) {
234            if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
235                return true;
236            }
237        }
238        return false;
239    }
240
241    private boolean accountExists(Account account) {
242        if (account == null) return false;
243
244        Account[] accounts = AccountManager.get(getActivity()).getAccountsByTypeAsUser(
245                account.type, mUserHandle);
246        for (final Account other : accounts) {
247            if (other.equals(account)) {
248                return true;
249            }
250        }
251        return false;
252    }
253
254    @Override
255    public void onAccountsUpdate(UserHandle userHandle) {
256        if (!isResumed()) {
257            return;
258        }
259        if (!accountExists(mAccount)) {
260            // The account was deleted
261            if (!getFragmentManager().popBackStackImmediate()) {
262                getActivity().finish();
263            }
264            return;
265        }
266        updateAccountSwitches();
267        onSyncStateUpdated();
268    }
269
270    private void onSyncStateUpdated() {
271        // iterate over all the preferences, setting the state properly for each
272        final int userId = mUserHandle.getIdentifier();
273        List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
274//        boolean syncIsFailing = false;
275
276        // Refresh the sync status switches - some syncs may have become active.
277        updateAccountSwitches();
278
279        for (int i = 0, count = mSyncCategory.getPreferenceCount(); i < count; i++) {
280            Preference pref = mSyncCategory.getPreference(i);
281            if (! (pref instanceof SyncStateSwitchPreference)) {
282                continue;
283            }
284            SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) pref;
285
286            String authority = syncPref.getAuthority();
287            Account account = syncPref.getAccount();
288
289            SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority, userId);
290            boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority,
291                    userId);
292            boolean authorityIsPending = status != null && status.pending;
293            boolean initialSync = status != null && status.initialize;
294
295            boolean activelySyncing = isSyncing(currentSyncs, account, authority);
296            boolean lastSyncFailed = status != null
297                    && status.lastFailureTime != 0
298                    && status.getLastFailureMesgAsInt(0)
299                    != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
300            if (!syncEnabled) lastSyncFailed = false;
301//            if (lastSyncFailed && !activelySyncing && !authorityIsPending) {
302//                syncIsFailing = true;
303//            }
304            if (Log.isLoggable(TAG, Log.VERBOSE)) {
305                Log.v(TAG, "Update sync status: " + account + " " + authority +
306                        " active = " + activelySyncing + " pend =" +  authorityIsPending);
307            }
308
309            final long successEndTime = (status == null) ? 0 : status.lastSuccessTime;
310            if (!syncEnabled) {
311                syncPref.setSummary(R.string.sync_disabled);
312            } else if (activelySyncing) {
313                syncPref.setSummary(R.string.sync_in_progress);
314            } else if (successEndTime != 0) {
315                final String timeString = DateUtils.formatDateTime(getActivity(), successEndTime,
316                        DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
317                syncPref.setSummary(getResources().getString(R.string.last_synced, timeString));
318            } else {
319                syncPref.setSummary("");
320            }
321            int syncState = ContentResolver.getIsSyncableAsUser(account, authority, userId);
322
323            syncPref.setActive(activelySyncing && (syncState >= 0) &&
324                    !initialSync);
325            syncPref.setPending(authorityIsPending && (syncState >= 0) &&
326                    !initialSync);
327
328            syncPref.setFailed(lastSyncFailed);
329            final boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser(
330                    userId);
331            syncPref.setOneTimeSyncMode(oneTimeSyncMode);
332            syncPref.setChecked(oneTimeSyncMode || syncEnabled);
333        }
334    }
335
336    private void updateAccountSwitches() {
337        mInvisibleAdapters.clear();
338
339        SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(
340                mUserHandle.getIdentifier());
341        ArrayList<String> authorities = new ArrayList<>(syncAdapters.length);
342        for (SyncAdapterType sa : syncAdapters) {
343            // Only keep track of sync adapters for this account
344            if (!sa.accountType.equals(mAccount.type)) continue;
345            if (sa.isUserVisible()) {
346                if (Log.isLoggable(TAG, Log.VERBOSE)) {
347                    Log.v(TAG, "updateAccountSwitches: added authority " + sa.authority
348                            + " to accountType " + sa.accountType);
349                }
350                authorities.add(sa.authority);
351            } else {
352                // keep track of invisible sync adapters, so sync now forces
353                // them to sync as well.
354                mInvisibleAdapters.add(sa);
355            }
356        }
357
358        mSyncCategory.removeAll();
359        final List<Preference> switches = new ArrayList<>(authorities.size());
360
361        if (Log.isLoggable(TAG, Log.VERBOSE)) {
362            Log.v(TAG, "looking for sync adapters that match account " + mAccount);
363        }
364        for (final String authority : authorities) {
365            // We could check services here....
366            int syncState = ContentResolver.getIsSyncableAsUser(mAccount, authority,
367                    mUserHandle.getIdentifier());
368            if (Log.isLoggable(TAG, Log.VERBOSE)) {
369                Log.v(TAG, "  found authority " + authority + " " + syncState);
370            }
371            if (syncState > 0) {
372                final Preference pref = createSyncStateSwitch(mAccount, authority);
373                switches.add(pref);
374            }
375        }
376
377        Collections.sort(switches);
378        for (final Preference pref : switches) {
379            mSyncCategory.addPreference(pref);
380        }
381    }
382
383    private Preference createSyncStateSwitch(Account account, String authority) {
384        final Context themedContext = getPreferenceManager().getContext();
385        SyncStateSwitchPreference preference =
386                new SyncStateSwitchPreference(themedContext, account, authority);
387        preference.setPersistent(false);
388        final PackageManager packageManager = getActivity().getPackageManager();
389        final ProviderInfo providerInfo = packageManager.resolveContentProviderAsUser(
390                authority, 0, mUserHandle.getIdentifier());
391        if (providerInfo == null) {
392            return null;
393        }
394        CharSequence providerLabel = providerInfo.loadLabel(packageManager);
395        if (TextUtils.isEmpty(providerLabel)) {
396            Log.e(TAG, "Provider needs a label for authority '" + authority + "'");
397            return null;
398        }
399        String title = getString(R.string.sync_item_title, providerLabel);
400        preference.setTitle(title);
401        preference.setKey(authority);
402        return preference;
403    }
404
405}
406