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