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 
44 import com.google.android.collect.Lists;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 
50 public 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 
newInstance(Account account)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 
prepareArgs(Bundle b, Account account)88     public static void prepareArgs(Bundle b, Account account) {
89         b.putParcelable(ARG_ACCOUNT, account);
90     }
91 
92     @Override
onCreate(Bundle savedInstanceState)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
onStart()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
onResume()119     public void onResume() {
120         super.onResume();
121         mHandler.post(() -> onAccountsUpdate(mUserHandle));
122     }
123 
124     @Override
onStop()125     public void onStop() {
126         super.onStop();
127         ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
128         mAuthenticatorHelper.stopListeningToAccountUpdates();
129     }
130 
131     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)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
onPreferenceTreeClick(Preference preference)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 
startSyncForEnabledProviders()183     private void startSyncForEnabledProviders() {
184         requestOrCancelSyncForEnabledProviders(true /* start them */);
185         final Activity activity = getActivity();
186         if (activity != null) {
187             activity.invalidateOptionsMenu();
188         }
189     }
190 
cancelSyncForEnabledProviders()191     private void cancelSyncForEnabledProviders() {
192         requestOrCancelSyncForEnabledProviders(false /* cancel them */);
193         final Activity activity = getActivity();
194         if (activity != null) {
195             activity.invalidateOptionsMenu();
196         }
197     }
198 
requestOrCancelSyncForEnabledProviders(boolean startSync)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 
requestOrCancelSync(Account account, String authority, boolean flag)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 
isSyncing(List<SyncInfo> currentSyncs, Account account, String authority)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 
accountExists(Account account)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
onAccountsUpdate(UserHandle userHandle)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 
onSyncStateUpdated()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 
updateAccountSwitches()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 
createSyncStateSwitch(Account account, String authority)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