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