• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.documentsui.roots;
18 
19 import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES;
20 
21 import static androidx.core.util.Preconditions.checkNotNull;
22 
23 import static com.android.documentsui.base.SharedMinimal.DEBUG;
24 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
25 
26 import android.content.BroadcastReceiver.PendingResult;
27 import android.content.ContentProviderClient;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ProviderInfo;
34 import android.content.pm.ResolveInfo;
35 import android.database.ContentObserver;
36 import android.database.Cursor;
37 import android.net.Uri;
38 import android.os.AsyncTask;
39 import android.os.Bundle;
40 import android.os.FileUtils;
41 import android.os.Handler;
42 import android.os.Looper;
43 import android.os.SystemClock;
44 import android.provider.DocumentsContract;
45 import android.provider.DocumentsContract.Root;
46 import android.util.Log;
47 
48 import androidx.annotation.GuardedBy;
49 import androidx.annotation.Nullable;
50 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
51 
52 import com.android.documentsui.DocumentsApplication;
53 import com.android.documentsui.R;
54 import com.android.documentsui.UserPackage;
55 import com.android.documentsui.archives.ArchivesProvider;
56 import com.android.documentsui.base.LookupApplicationName;
57 import com.android.documentsui.base.Providers;
58 import com.android.documentsui.base.RootInfo;
59 import com.android.documentsui.base.State;
60 import com.android.documentsui.base.UserId;
61 import com.android.modules.utils.build.SdkLevel;
62 
63 import com.google.common.collect.ArrayListMultimap;
64 import com.google.common.collect.Multimap;
65 import com.google.common.util.concurrent.MoreExecutors;
66 
67 import java.util.ArrayList;
68 import java.util.Collection;
69 import java.util.Collections;
70 import java.util.HashMap;
71 import java.util.HashSet;
72 import java.util.List;
73 import java.util.Map;
74 import java.util.Objects;
75 import java.util.concurrent.CountDownLatch;
76 import java.util.concurrent.ExecutorService;
77 import java.util.concurrent.Executors;
78 import java.util.concurrent.Semaphore;
79 import java.util.concurrent.ThreadPoolExecutor;
80 import java.util.concurrent.TimeUnit;
81 import java.util.function.Function;
82 
83 /**
84  * Cache of known storage backends and their roots.
85  */
86 public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
87     private static final String TAG = "ProvidersCache";
88 
89     // Not all providers are equally well written. If a provider returns
90     // empty results we don't cache them...unless they're in this magical list
91     // of beloved providers.
92     private static final List<String> PERMIT_EMPTY_CACHE = List.of(
93             // MTP provider commonly returns no roots (if no devices are attached).
94             Providers.AUTHORITY_MTP,
95             // ArchivesProvider doesn't support any roots.
96             ArchivesProvider.AUTHORITY);
97     private static final int FIRST_LOAD_TIMEOUT_MS = 5000;
98 
99     private final Context mContext;
100 
101     @GuardedBy("mRootsChangedObservers")
102     private final Map<UserId, RootsChangedObserver> mRootsChangedObservers = new HashMap<>();
103 
104     @GuardedBy("mRecentsRoots")
105     private final Map<UserId, RootInfo> mRecentsRoots = new HashMap<>();
106 
107     private final Object mLock = new Object();
108     private final CountDownLatch mFirstLoad = new CountDownLatch(1);
109 
110     @GuardedBy("mLock")
111     private boolean mFirstLoadDone;
112     @GuardedBy("mLock")
113     private PendingResult mBootCompletedResult;
114 
115     @GuardedBy("mLock")
116     private Multimap<UserAuthority, RootInfo> mRoots = ArrayListMultimap.create();
117     @GuardedBy("mLock")
118     private HashSet<UserAuthority> mStoppedAuthorities = new HashSet<>();
119     private final Semaphore mMultiProviderUpdateTaskSemaphore = new Semaphore(1);
120 
121     @GuardedBy("mObservedAuthoritiesDetails")
122     private final Map<UserAuthority, PackageDetails> mObservedAuthoritiesDetails = new HashMap<>();
123 
ProvidersCache(Context context)124     public ProvidersCache(Context context) {
125         mContext = context;
126     }
127 
128     /**
129      * Generates recent root for the provided user id
130      */
generateRecentsRoot(UserId rootUserId)131     private RootInfo generateRecentsRoot(UserId rootUserId) {
132         return new RootInfo() {{
133             // Special root for recents
134             userId = rootUserId;
135             derivedIcon = R.drawable.ic_root_recent;
136             derivedType = RootInfo.TYPE_RECENTS;
137             flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH;
138             queryArgs = QUERY_ARG_MIME_TYPES;
139             title = mContext.getString(R.string.root_recent);
140             availableBytes = -1;
141         }};
142     }
143 
144     private RootInfo createOrGetRecentsRoot(UserId userId) {
145         return createOrGetByUserId(mRecentsRoots, userId, user -> generateRecentsRoot(user));
146     }
147 
148     private RootsChangedObserver createOrGetRootsChangedObserver(UserId userId) {
149         return createOrGetByUserId(mRootsChangedObservers, userId,
150                 user -> new RootsChangedObserver(user));
151     }
152 
153     private static <T> T createOrGetByUserId(Map<UserId, T> map, UserId userId,
154             Function<UserId, T> supplier) {
155         synchronized (map) {
156             if (!map.containsKey(userId)) {
157                 map.put(userId, supplier.apply(userId));
158             }
159         }
160         return map.get(userId);
161     }
162 
163     private class RootsChangedObserver extends ContentObserver {
164 
165         private final UserId mUserId;
166 
167         RootsChangedObserver(UserId userId) {
168             super(new Handler(Looper.getMainLooper()));
169             mUserId = userId;
170         }
171 
172         @Override
173         public void onChange(boolean selfChange, Uri uri) {
174             if (uri == null) {
175                 Log.w(TAG, "Received onChange event for null uri. Skipping.");
176                 return;
177             }
178             if (DEBUG) {
179                 Log.i(TAG, "Updating roots due to change on user " + mUserId + "at " + uri);
180             }
181             updateAuthorityAsync(mUserId, uri.getAuthority());
182         }
183     }
184 
185     @Override
186     public String getApplicationName(UserId userId, String authority) {
187         return mObservedAuthoritiesDetails.get(
188                 new UserAuthority(userId, authority)).applicationName;
189     }
190 
191     @Override
192     public String getPackageName(UserId userId, String authority) {
193         return mObservedAuthoritiesDetails.get(new UserAuthority(userId, authority)).packageName;
194     }
195 
196     public void updateAsync(boolean forceRefreshAll, @Nullable Runnable callback) {
197 
198         // NOTE: This method is called when the UI language changes.
199         // For that reason we update our RecentsRoot to reflect
200         // the current language.
201         final String title = mContext.getString(R.string.root_recent);
202         List<UserId> userIds = new ArrayList<>(getUserIds());
203         for (UserId userId : userIds) {
204             RootInfo recentRoot = createOrGetRecentsRoot(userId);
205             recentRoot.title = title;
206             // Nothing else about the root should ever change.
207             assert (recentRoot.authority == null);
208             assert (recentRoot.rootId == null);
209             assert (recentRoot.derivedIcon == R.drawable.ic_root_recent);
210             assert (recentRoot.derivedType == RootInfo.TYPE_RECENTS);
211             assert (recentRoot.flags == (Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD));
212             assert (recentRoot.availableBytes == -1);
213         }
214 
215         new MultiProviderUpdateTask(forceRefreshAll, null, callback).executeOnExecutor(
216                 AsyncTask.THREAD_POOL_EXECUTOR);
217     }
218 
219     public void updatePackageAsync(UserId userId, String packageName) {
220         new MultiProviderUpdateTask(
221                 /* forceRefreshAll= */ false,
222                 new UserPackage(userId, packageName),
223                 /* callback= */ null)
224                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
225     }
226 
227     public void updateAuthorityAsync(UserId userId, String authority) {
228         final ProviderInfo info = userId.getPackageManager(mContext).resolveContentProvider(
229                 authority, 0);
230         if (info != null) {
231             updatePackageAsync(userId, info.packageName);
232         }
233     }
234 
235     void setBootCompletedResult(PendingResult result) {
236         synchronized (mLock) {
237             // Quickly check if we've already finished loading, otherwise hang
238             // out until first pass is finished.
239             if (mFirstLoadDone) {
240                 result.finish();
241             } else {
242                 mBootCompletedResult = result;
243             }
244         }
245     }
246 
247     /**
248      * Block until the first {@link MultiProviderUpdateTask} pass has finished.
249      *
250      * @return {@code true} if cached roots is ready to roll, otherwise
251      * {@code false} if we timed out while waiting.
252      */
253     private boolean waitForFirstLoad() {
254         boolean success = false;
255         try {
256             success = mFirstLoad.await(FIRST_LOAD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
257         } catch (InterruptedException e) {
258         }
259         if (!success) {
260             Log.w(TAG, "Timeout waiting for first update");
261         }
262         return success;
263     }
264 
265     /**
266      * Load roots from authorities that are in stopped state. Normal
267      * {@link MultiProviderUpdateTask} passes ignore stopped applications.
268      */
269     private void loadStoppedAuthorities() {
270         synchronized (mLock) {
271             for (UserAuthority userAuthority : mStoppedAuthorities) {
272                 mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true));
273             }
274             mStoppedAuthorities.clear();
275         }
276     }
277 
278     /**
279      * Load roots from a stopped authority. Normal {@link MultiProviderUpdateTask} passes
280      * ignore stopped applications.
281      */
282     private void loadStoppedAuthority(UserAuthority userAuthority) {
283         synchronized (mLock) {
284             if (!mStoppedAuthorities.contains(userAuthority)) {
285                 return;
286             }
287             if (DEBUG) {
288                 Log.d(TAG, "Loading stopped authority " + userAuthority);
289             }
290             mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true));
291             mStoppedAuthorities.remove(userAuthority);
292         }
293     }
294 
295     /**
296      * Bring up requested provider and query for all active roots. Will consult cached
297      * roots if not forceRefresh. Will query when cached roots is empty (which should never happen).
298      */
299     private Collection<RootInfo> loadRootsForAuthority(UserAuthority userAuthority,
300             boolean forceRefresh) {
301         UserId userId = userAuthority.userId;
302         String authority = userAuthority.authority;
303         if (VERBOSE) Log.v(TAG, "Loading roots on user " + userId + " for " + authority);
304 
305         ContentResolver resolver = userId.getContentResolver(mContext);
306         final ArrayList<RootInfo> roots = new ArrayList<>();
307         final PackageManager pm = userId.getPackageManager(mContext);
308         ProviderInfo provider = pm.resolveContentProvider(
309                 authority, PackageManager.GET_META_DATA);
310         if (provider == null) {
311             Log.w(TAG, "Failed to get provider info for " + authority);
312             return roots;
313         }
314         if (!provider.exported) {
315             Log.w(TAG, "Provider is not exported. Failed to load roots for " + authority);
316             return roots;
317         }
318         if (!provider.grantUriPermissions) {
319             Log.w(TAG, "Provider doesn't grantUriPermissions. Failed to load roots for "
320                     + authority);
321             return roots;
322         }
323         if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.readPermission)
324                 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.writePermission)) {
325             Log.w(TAG, "Provider is not protected by MANAGE_DOCUMENTS. Failed to load roots for "
326                     + authority);
327             return roots;
328         }
329 
330         synchronized (mObservedAuthoritiesDetails) {
331             if (!mObservedAuthoritiesDetails.containsKey(userAuthority)) {
332                 CharSequence appName = pm.getApplicationLabel(provider.applicationInfo);
333                 String packageName = provider.applicationInfo.packageName;
334 
335                 mObservedAuthoritiesDetails.put(
336                         userAuthority, new PackageDetails(appName.toString(), packageName));
337 
338                 // Watch for any future updates
339                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
340                 resolver.registerContentObserver(rootsUri, true,
341                         createOrGetRootsChangedObserver(userId));
342             }
343         }
344 
345         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
346         if (!forceRefresh) {
347             // Look for roots data that we might have cached for ourselves in the
348             // long-lived system process.
349             final Bundle systemCache = resolver.getCache(rootsUri);
350             if (systemCache != null) {
351                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
352                 assert (cachedRoots != null);
353                 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) {
354                     if (VERBOSE) Log.v(TAG, "System cache hit for " + authority);
355                     return cachedRoots;
356                 } else {
357                     Log.w(TAG, "Ignoring empty system cache hit for " + authority);
358                 }
359             }
360         }
361 
362         ContentProviderClient client = null;
363         Cursor cursor = null;
364         try {
365             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
366             cursor = client.query(rootsUri, null, null, null, null);
367             while (cursor.moveToNext()) {
368                 final RootInfo root = RootInfo.fromRootsCursor(userId, authority, cursor);
369                 roots.add(root);
370             }
371         } catch (Exception e) {
372             Log.w(TAG, "Failed to load some roots from " + authority, e);
373             // We didn't load every root from the provider. Don't put it to
374             // system cache so that we'll try loading them again next time even
375             // if forceRefresh is false.
376             return roots;
377         } finally {
378             FileUtils.closeQuietly(cursor);
379             FileUtils.closeQuietly(client);
380         }
381 
382         // Cache these freshly parsed roots over in the long-lived system
383         // process, in case our process goes away. The system takes care of
384         // invalidating the cache if the package or Uri changes.
385         final Bundle systemCache = new Bundle();
386         if (roots.isEmpty() && !PERMIT_EMPTY_CACHE.contains(authority)) {
387             Log.i(TAG, "Provider returned no roots. Possibly naughty: " + authority);
388         } else {
389             systemCache.putParcelableArrayList(TAG, roots);
390             resolver.putCache(rootsUri, systemCache);
391         }
392 
393         return roots;
394     }
395 
396     @Override
397     public RootInfo getRootOneshot(UserId userId, String authority, String rootId) {
398         return getRootOneshot(userId, authority, rootId, false);
399     }
400 
401     public RootInfo getRootOneshot(UserId userId, String authority, String rootId,
402             boolean forceRefresh) {
403         synchronized (mLock) {
404             UserAuthority userAuthority = new UserAuthority(userId, authority);
405             RootInfo root = forceRefresh ? null : getRootLocked(userAuthority, rootId);
406             if (root == null) {
407                 mRoots.replaceValues(userAuthority,
408                         loadRootsForAuthority(userAuthority, forceRefresh));
409                 root = getRootLocked(userAuthority, rootId);
410             }
411             return root;
412         }
413     }
414 
415     public RootInfo getRootBlocking(UserId userId, String authority, String rootId) {
416         waitForFirstLoad();
417         loadStoppedAuthorities();
418         synchronized (mLock) {
419             return getRootLocked(new UserAuthority(userId, authority), rootId);
420         }
421     }
422 
423     private RootInfo getRootLocked(UserAuthority userAuthority, String rootId) {
424         for (RootInfo root : mRoots.get(userAuthority)) {
425             if (Objects.equals(root.rootId, rootId)) {
426                 return root;
427             }
428         }
429         return null;
430     }
431 
432     @Override
433     public RootInfo getRecentsRoot(UserId userId) {
434         return createOrGetRecentsRoot(userId);
435     }
436 
437     public boolean isRecentsRoot(RootInfo root) {
438         return mRecentsRoots.containsValue(root);
439     }
440 
441     @Override
442     public Collection<RootInfo> getRootsBlocking() {
443         waitForFirstLoad();
444         loadStoppedAuthorities();
445         synchronized (mLock) {
446             return new HashSet<>(mRoots.values());
447         }
448     }
449 
450     @Override
451     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
452         waitForFirstLoad();
453         loadStoppedAuthorities();
454         synchronized (mLock) {
455             return ProvidersAccess.getMatchingRoots(mRoots.values(), state);
456         }
457     }
458 
459     @Override
460     public Collection<RootInfo> getRootsForAuthorityBlocking(UserId userId, String authority) {
461         waitForFirstLoad();
462         UserAuthority userAuthority = new UserAuthority(userId, authority);
463         loadStoppedAuthority(userAuthority);
464         synchronized (mLock) {
465             final Collection<RootInfo> roots = mRoots.get(userAuthority);
466             return roots != null ? roots : Collections.<RootInfo>emptyList();
467         }
468     }
469 
470     @Override
471     public RootInfo getDefaultRootBlocking(State state) {
472         RootInfo root = ProvidersAccess.getDefaultRoot(getRootsBlocking(), state);
473         return root != null ? root : createOrGetRecentsRoot(UserId.CURRENT_USER);
474     }
475 
476     public void logCache() {
477         StringBuilder output = new StringBuilder();
478 
479         for (UserAuthority userAuthority : mObservedAuthoritiesDetails.keySet()) {
480             List<String> roots = new ArrayList<>();
481             Uri rootsUri = DocumentsContract.buildRootsUri(userAuthority.authority);
482             Bundle systemCache = userAuthority.userId.getContentResolver(mContext).getCache(
483                     rootsUri);
484             if (systemCache != null) {
485                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
486                 for (RootInfo root : cachedRoots) {
487                     roots.add(root.toDebugString());
488                 }
489             }
490 
491             output.append((output.length() == 0) ? "System cache: " : ", ");
492             output.append(userAuthority).append("=").append(roots);
493         }
494 
495         Log.i(TAG, output.toString());
496     }
497 
498     private class MultiProviderUpdateTask extends AsyncTask<Void, Void, Void> {
499         private final boolean mForceRefreshAll;
500         @Nullable
501         private final UserPackage mForceRefreshUserPackage;
502         @Nullable
503         private final Runnable mCallback;
504 
505         @GuardedBy("mLock")
506         private Multimap<UserAuthority, RootInfo> mLocalRoots = ArrayListMultimap.create();
507         @GuardedBy("mLock")
508         private HashSet<UserAuthority> mLocalStoppedAuthorities = new HashSet<>();
509 
510         /**
511          * Create task to update roots cache.
512          *
513          * @param forceRefreshAll         when true, all previously cached values for
514          *                                all packages should be ignored.
515          * @param forceRefreshUserPackage when non-null, all previously cached
516          *                                values for this specific user package should be ignored.
517          * @param callback                when non-null, it will be invoked after the task is
518          *                                executed.
519          */
520         MultiProviderUpdateTask(
521                 boolean forceRefreshAll,
522                 @Nullable UserPackage forceRefreshUserPackage,
523                 @Nullable Runnable callback) {
524             mForceRefreshAll = forceRefreshAll;
525             mForceRefreshUserPackage = forceRefreshUserPackage;
526             mCallback = callback;
527         }
528 
529         @Override
530         protected Void doInBackground(Void... params) {
531             if (!mMultiProviderUpdateTaskSemaphore.tryAcquire()) {
532                 // Abort, since previous update task is still running.
533                 return null;
534             }
535 
536             int previousPriority = Thread.currentThread().getPriority();
537             Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
538 
539             final long start = SystemClock.elapsedRealtime();
540 
541             List<UserId> userIds = new ArrayList<>(getUserIds());
542             for (UserId userId : userIds) {
543                 final RootInfo recents = createOrGetRecentsRoot(userId);
544                 synchronized (mLock) {
545                     mLocalRoots.put(new UserAuthority(recents.userId, recents.authority), recents);
546                 }
547             }
548 
549             List<SingleProviderUpdateTaskInfo> taskInfos = new ArrayList<>();
550             for (UserId userId : userIds) {
551                 final PackageManager pm = userId.getPackageManager(mContext);
552                 // Pick up provider with action string
553                 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
554                 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
555                 for (ResolveInfo info : providers) {
556                     ProviderInfo providerInfo = info.providerInfo;
557                     if (providerInfo.authority != null) {
558                         taskInfos.add(new SingleProviderUpdateTaskInfo(providerInfo, userId));
559                     }
560                 }
561             }
562 
563             if (!taskInfos.isEmpty()) {
564                 CountDownLatch updateTaskInternalCountDown = new CountDownLatch(taskInfos.size());
565                 ExecutorService executor = MoreExecutors.getExitingExecutorService(
566                         (ThreadPoolExecutor) Executors.newCachedThreadPool());
567                 for (SingleProviderUpdateTaskInfo taskInfo : taskInfos) {
568                     executor.submit(() ->
569                             startSingleProviderUpdateTask(
570                                     taskInfo.providerInfo,
571                                     taskInfo.userId,
572                                     updateTaskInternalCountDown));
573                 }
574 
575                 // Block until all SingleProviderUpdateTask threads finish executing.
576                 // Use a shorter timeout for first load since it could block picker UI.
577                 long timeoutMs = mFirstLoadDone ? 15000 : FIRST_LOAD_TIMEOUT_MS;
578                 boolean success = false;
579                 try {
580                     success = updateTaskInternalCountDown.await(timeoutMs, TimeUnit.MILLISECONDS);
581                 } catch (InterruptedException e) {
582                 }
583                 if (!success) {
584                     Log.w(TAG, "Timeout executing update task!");
585                 }
586             }
587 
588             final long delta = SystemClock.elapsedRealtime() - start;
589             synchronized (mLock) {
590                 mFirstLoadDone = true;
591                 if (mBootCompletedResult != null) {
592                     mBootCompletedResult.finish();
593                     mBootCompletedResult = null;
594                 }
595                 mRoots = mLocalRoots;
596                 mStoppedAuthorities = mLocalStoppedAuthorities;
597             }
598             if (VERBOSE) {
599                 Log.v(TAG, "Update found " + mLocalRoots.size() + " roots in " + delta + "ms");
600             }
601 
602             mFirstLoad.countDown();
603             LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION));
604             mMultiProviderUpdateTaskSemaphore.release();
605 
606             Thread.currentThread().setPriority(previousPriority);
607             return null;
608         }
609 
610         @Override
611         protected void onPostExecute(Void aVoid) {
612             if (mCallback != null) {
613                 mCallback.run();
614             }
615         }
616 
617         private void startSingleProviderUpdateTask(
618                 ProviderInfo providerInfo,
619                 UserId userId,
620                 CountDownLatch updateCountDown) {
621             int previousPriority = Thread.currentThread().getPriority();
622             Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
623             handleDocumentsProvider(providerInfo, userId);
624             updateCountDown.countDown();
625             Thread.currentThread().setPriority(previousPriority);
626         }
627 
628         private void handleDocumentsProvider(ProviderInfo info, UserId userId) {
629             UserAuthority userAuthority = new UserAuthority(userId, info.authority);
630             // Ignore stopped packages for now; we might query them
631             // later during UI interaction.
632             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
633                 if (VERBOSE) {
634                     Log.v(TAG, "Ignoring stopped authority " + info.authority + ", user " + userId);
635                 }
636                 synchronized (mLock) {
637                     mLocalStoppedAuthorities.add(userAuthority);
638                 }
639                 return;
640             }
641 
642             final boolean forceRefresh = mForceRefreshAll
643                     || Objects.equals(
644                     new UserPackage(userId, info.packageName), mForceRefreshUserPackage);
645             synchronized (mLock) {
646                 mLocalRoots.putAll(userAuthority,
647                         loadRootsForAuthority(userAuthority, forceRefresh));
648             }
649         }
650     }
651 
652     private static class UserAuthority {
653         private final UserId userId;
654         @Nullable
655         private final String authority;
656 
657         private UserAuthority(UserId userId, @Nullable String authority) {
658             this.userId = checkNotNull(userId);
659             this.authority = authority;
660         }
661 
662         @Override
663         public boolean equals(Object o) {
664             if (o == null) {
665                 return false;
666             }
667 
668             if (this == o) {
669                 return true;
670             }
671 
672             if (o instanceof UserAuthority) {
673                 UserAuthority other = (UserAuthority) o;
674                 return Objects.equals(userId, other.userId)
675                         && Objects.equals(authority, other.authority);
676             }
677 
678             return false;
679         }
680 
681 
682         @Override
683         public int hashCode() {
684             return Objects.hash(userId, authority);
685         }
686     }
687 
688     private static class SingleProviderUpdateTaskInfo {
689         private final ProviderInfo providerInfo;
690         private final UserId userId;
691 
692         SingleProviderUpdateTaskInfo(ProviderInfo providerInfo, UserId userId) {
693             this.providerInfo = providerInfo;
694             this.userId = userId;
695         }
696     }
697 
698     private static class PackageDetails {
699         private String applicationName;
700         private String packageName;
701 
702         public PackageDetails(String appName, String pckgName) {
703             applicationName = appName;
704             packageName = pckgName;
705         }
706     }
707 
708     private List<UserId> getUserIds() {
709         if (DocumentsApplication.getConfigStore().isPrivateSpaceInDocsUIEnabled()
710                 && SdkLevel.isAtLeastS()) {
711             return DocumentsApplication.getUserManagerState(mContext).getUserIds();
712         }
713         return DocumentsApplication.getUserIdManager(mContext).getUserIds();
714     }
715 }
716