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