• 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 
66 import java.util.ArrayList;
67 import java.util.Collection;
68 import java.util.Collections;
69 import java.util.HashMap;
70 import java.util.HashSet;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.Objects;
74 import java.util.concurrent.CountDownLatch;
75 import java.util.concurrent.ExecutorService;
76 import java.util.concurrent.Executors;
77 import java.util.concurrent.Semaphore;
78 import java.util.concurrent.TimeUnit;
79 import java.util.function.Function;
80 
81 /**
82  * Cache of known storage backends and their roots.
83  */
84 public class ProvidersCache implements ProvidersAccess, LookupApplicationName {
85     private static final String TAG = "ProvidersCache";
86 
87     // Not all providers are equally well written. If a provider returns
88     // empty results we don't cache them...unless they're in this magical list
89     // of beloved providers.
90     private static final List<String> PERMIT_EMPTY_CACHE = List.of(
91             // MTP provider commonly returns no roots (if no devices are attached).
92             Providers.AUTHORITY_MTP,
93             // ArchivesProvider doesn't support any roots.
94             ArchivesProvider.AUTHORITY);
95     private static final int FIRST_LOAD_TIMEOUT_MS = 5000;
96     private static final int NUM_THREADS = 10;
97     private static final ExecutorService ASYNC_TASKS_THREAD_POOL =
98             Executors.newFixedThreadPool(NUM_THREADS);
99 
100     private final Context mContext;
101 
102     @GuardedBy("mRootsChangedObservers")
103     private final Map<UserId, RootsChangedObserver> mRootsChangedObservers = new HashMap<>();
104 
105     @GuardedBy("mRecentsRoots")
106     private final Map<UserId, RootInfo> mRecentsRoots = new HashMap<>();
107 
108     private final Object mLock = new Object();
109     private final CountDownLatch mFirstLoad = new CountDownLatch(1);
110 
111     @GuardedBy("mLock")
112     private boolean mFirstLoadDone;
113     @GuardedBy("mLock")
114     private PendingResult mBootCompletedResult;
115 
116     @GuardedBy("mLock")
117     private Multimap<UserAuthority, RootInfo> mRoots = ArrayListMultimap.create();
118     @GuardedBy("mLock")
119     private HashSet<UserAuthority> mStoppedAuthorities = new HashSet<>();
120     private final Semaphore mMultiProviderUpdateTaskSemaphore = new Semaphore(1);
121 
122     @GuardedBy("mObservedAuthoritiesDetails")
123     private final Map<UserAuthority, PackageDetails> mObservedAuthoritiesDetails = new HashMap<>();
124 
ProvidersCache(Context context)125     public ProvidersCache(Context context) {
126         mContext = context;
127     }
128 
129     /**
130      * Generates recent root for the provided user id
131      */
generateRecentsRoot(UserId rootUserId)132     private RootInfo generateRecentsRoot(UserId rootUserId) {
133         return new RootInfo() {{
134             // Special root for recents
135             userId = rootUserId;
136             derivedIcon = R.drawable.ic_root_recent;
137             derivedType = RootInfo.TYPE_RECENTS;
138             flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH;
139             queryArgs = QUERY_ARG_MIME_TYPES;
140             title = mContext.getString(R.string.root_recent);
141             availableBytes = -1;
142         }};
143     }
144 
145     private RootInfo createOrGetRecentsRoot(UserId userId) {
146         return createOrGetByUserId(mRecentsRoots, userId, user -> generateRecentsRoot(user));
147     }
148 
149     private RootsChangedObserver createOrGetRootsChangedObserver(UserId userId) {
150         return createOrGetByUserId(mRootsChangedObservers, userId,
151                 user -> new RootsChangedObserver(user));
152     }
153 
154     private static <T> T createOrGetByUserId(Map<UserId, T> map, UserId userId,
155             Function<UserId, T> supplier) {
156         synchronized (map) {
157             if (!map.containsKey(userId)) {
158                 map.put(userId, supplier.apply(userId));
159             }
160         }
161         return map.get(userId);
162     }
163 
164     private class RootsChangedObserver extends ContentObserver {
165 
166         private final UserId mUserId;
167 
168         RootsChangedObserver(UserId userId) {
169             super(new Handler(Looper.getMainLooper()));
170             mUserId = userId;
171         }
172 
173         @Override
174         public void onChange(boolean selfChange, Uri uri) {
175             if (uri == null) {
176                 Log.w(TAG, "Received onChange event for null uri. Skipping.");
177                 return;
178             }
179             if (DEBUG) {
180                 Log.i(TAG, "Updating roots due to change on user " + mUserId + "at " + uri);
181             }
182             updateAuthorityAsync(mUserId, uri.getAuthority());
183         }
184     }
185 
186     @Override
187     public String getApplicationName(UserId userId, String authority) {
188         return mObservedAuthoritiesDetails.get(
189                 new UserAuthority(userId, authority)).applicationName;
190     }
191 
192     @Override
193     public String getPackageName(UserId userId, String authority) {
194         return mObservedAuthoritiesDetails.get(new UserAuthority(userId, authority)).packageName;
195     }
196 
197     public void updateAsync(boolean forceRefreshAll, @Nullable Runnable callback) {
198 
199         // NOTE: This method is called when the UI language changes.
200         // For that reason we update our RecentsRoot to reflect
201         // the current language.
202         final String title = mContext.getString(R.string.root_recent);
203         List<UserId> userIds = new ArrayList<>(getUserIds());
204         for (UserId userId : userIds) {
205             RootInfo recentRoot = createOrGetRecentsRoot(userId);
206             recentRoot.title = title;
207             // Nothing else about the root should ever change.
208             assert (recentRoot.authority == null);
209             assert (recentRoot.rootId == null);
210             assert (recentRoot.derivedIcon == R.drawable.ic_root_recent);
211             assert (recentRoot.derivedType == RootInfo.TYPE_RECENTS);
212             assert (recentRoot.flags == (Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD));
213             assert (recentRoot.availableBytes == -1);
214         }
215 
216         new MultiProviderUpdateTask(forceRefreshAll, null, callback).executeOnExecutor(
217                 AsyncTask.THREAD_POOL_EXECUTOR);
218     }
219 
220     public void updatePackageAsync(UserId userId, String packageName) {
221         new MultiProviderUpdateTask(
222                 /* forceRefreshAll= */ false,
223                 new UserPackage(userId, packageName),
224                 /* callback= */ null)
225                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
226     }
227 
228     public void updateAuthorityAsync(UserId userId, String authority) {
229         final ProviderInfo info = userId.getPackageManager(mContext).resolveContentProvider(
230                 authority, 0);
231         if (info != null) {
232             updatePackageAsync(userId, info.packageName);
233         }
234     }
235 
236     void setBootCompletedResult(PendingResult result) {
237         synchronized (mLock) {
238             // Quickly check if we've already finished loading, otherwise hang
239             // out until first pass is finished.
240             if (mFirstLoadDone) {
241                 result.finish();
242             } else {
243                 mBootCompletedResult = result;
244             }
245         }
246     }
247 
248     /**
249      * Block until the first {@link MultiProviderUpdateTask} pass has finished.
250      *
251      * @return {@code true} if cached roots is ready to roll, otherwise
252      * {@code false} if we timed out while waiting.
253      */
254     private boolean waitForFirstLoad() {
255         boolean success = false;
256         try {
257             success = mFirstLoad.await(FIRST_LOAD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
258         } catch (InterruptedException e) {
259         }
260         if (!success) {
261             Log.w(TAG, "Timeout waiting for first update");
262         }
263         return success;
264     }
265 
266     /**
267      * Load roots from authorities that are in stopped state. Normal
268      * {@link MultiProviderUpdateTask} passes ignore stopped applications.
269      */
270     private void loadStoppedAuthorities() {
271         synchronized (mLock) {
272             for (UserAuthority userAuthority : mStoppedAuthorities) {
273                 mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true));
274             }
275             mStoppedAuthorities.clear();
276         }
277     }
278 
279     /**
280      * Load roots from a stopped authority. Normal {@link MultiProviderUpdateTask} passes
281      * ignore stopped applications.
282      */
283     private void loadStoppedAuthority(UserAuthority userAuthority) {
284         synchronized (mLock) {
285             if (!mStoppedAuthorities.contains(userAuthority)) {
286                 return;
287             }
288             if (DEBUG) {
289                 Log.d(TAG, "Loading stopped authority " + userAuthority);
290             }
291             mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true));
292             mStoppedAuthorities.remove(userAuthority);
293         }
294     }
295 
296     /**
297      * Bring up requested provider and query for all active roots. Will consult cached
298      * roots if not forceRefresh. Will query when cached roots is empty (which should never happen).
299      */
300     private Collection<RootInfo> loadRootsForAuthority(UserAuthority userAuthority,
301             boolean forceRefresh) {
302         UserId userId = userAuthority.userId;
303         String authority = userAuthority.authority;
304         if (VERBOSE) Log.v(TAG, "Loading roots on user " + userId + " for " + authority);
305 
306         ContentResolver resolver = userId.getContentResolver(mContext);
307         final ArrayList<RootInfo> roots = new ArrayList<>();
308         final PackageManager pm = userId.getPackageManager(mContext);
309         ProviderInfo provider = pm.resolveContentProvider(
310                 authority, PackageManager.GET_META_DATA);
311         if (provider == null) {
312             Log.w(TAG, "Failed to get provider info for " + authority);
313             return roots;
314         }
315         if (!provider.exported) {
316             Log.w(TAG, "Provider is not exported. Failed to load roots for " + authority);
317             return roots;
318         }
319         if (!provider.grantUriPermissions) {
320             Log.w(TAG, "Provider doesn't grantUriPermissions. Failed to load roots for "
321                     + authority);
322             return roots;
323         }
324         if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.readPermission)
325                 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.writePermission)) {
326             Log.w(TAG, "Provider is not protected by MANAGE_DOCUMENTS. Failed to load roots for "
327                     + authority);
328             return roots;
329         }
330 
331         synchronized (mObservedAuthoritiesDetails) {
332             if (!mObservedAuthoritiesDetails.containsKey(userAuthority)) {
333                 CharSequence appName = pm.getApplicationLabel(provider.applicationInfo);
334                 String packageName = provider.applicationInfo.packageName;
335 
336                 mObservedAuthoritiesDetails.put(
337                         userAuthority, new PackageDetails(appName.toString(), packageName));
338 
339                 // Watch for any future updates
340                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
341                 resolver.registerContentObserver(rootsUri, true,
342                         createOrGetRootsChangedObserver(userId));
343             }
344         }
345 
346         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
347         if (!forceRefresh) {
348             // Look for roots data that we might have cached for ourselves in the
349             // long-lived system process.
350             final Bundle systemCache = resolver.getCache(rootsUri);
351             if (systemCache != null) {
352                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
353                 assert (cachedRoots != null);
354                 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) {
355                     if (VERBOSE) Log.v(TAG, "System cache hit for " + authority);
356                     return cachedRoots;
357                 } else {
358                     Log.w(TAG, "Ignoring empty system cache hit for " + authority);
359                 }
360             }
361         }
362 
363         ContentProviderClient client = null;
364         Cursor cursor = null;
365         try {
366             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
367             cursor = client.query(rootsUri, null, null, null, null);
368             while (cursor.moveToNext()) {
369                 final RootInfo root = RootInfo.fromRootsCursor(userId, authority, cursor);
370                 roots.add(root);
371             }
372         } catch (Exception e) {
373             Log.w(TAG, "Failed to load some roots from " + authority, e);
374             // We didn't load every root from the provider. Don't put it to
375             // system cache so that we'll try loading them again next time even
376             // if forceRefresh is false.
377             return roots;
378         } finally {
379             FileUtils.closeQuietly(cursor);
380             FileUtils.closeQuietly(client);
381         }
382 
383         // Cache these freshly parsed roots over in the long-lived system
384         // process, in case our process goes away. The system takes care of
385         // invalidating the cache if the package or Uri changes.
386         final Bundle systemCache = new Bundle();
387         if (roots.isEmpty() && !PERMIT_EMPTY_CACHE.contains(authority)) {
388             Log.i(TAG, "Provider returned no roots. Possibly naughty: " + authority);
389         } else {
390             systemCache.putParcelableArrayList(TAG, roots);
391             resolver.putCache(rootsUri, systemCache);
392         }
393 
394         return roots;
395     }
396 
397     @Override
398     public RootInfo getRootOneshot(UserId userId, String authority, String rootId) {
399         return getRootOneshot(userId, authority, rootId, false);
400     }
401 
402     public RootInfo getRootOneshot(UserId userId, String authority, String rootId,
403             boolean forceRefresh) {
404         synchronized (mLock) {
405             UserAuthority userAuthority = new UserAuthority(userId, authority);
406             RootInfo root = forceRefresh ? null : getRootLocked(userAuthority, rootId);
407             if (root == null) {
408                 mRoots.replaceValues(userAuthority,
409                         loadRootsForAuthority(userAuthority, forceRefresh));
410                 root = getRootLocked(userAuthority, rootId);
411             }
412             return root;
413         }
414     }
415 
416     public RootInfo getRootBlocking(UserId userId, String authority, String rootId) {
417         waitForFirstLoad();
418         loadStoppedAuthorities();
419         synchronized (mLock) {
420             return getRootLocked(new UserAuthority(userId, authority), rootId);
421         }
422     }
423 
424     private RootInfo getRootLocked(UserAuthority userAuthority, String rootId) {
425         for (RootInfo root : mRoots.get(userAuthority)) {
426             if (Objects.equals(root.rootId, rootId)) {
427                 return root;
428             }
429         }
430         return null;
431     }
432 
433     @Override
434     public RootInfo getRecentsRoot(UserId userId) {
435         return createOrGetRecentsRoot(userId);
436     }
437 
438     public boolean isRecentsRoot(RootInfo root) {
439         return mRecentsRoots.containsValue(root);
440     }
441 
442     @Override
443     public Collection<RootInfo> getRootsBlocking() {
444         waitForFirstLoad();
445         loadStoppedAuthorities();
446         synchronized (mLock) {
447             return new HashSet<>(mRoots.values());
448         }
449     }
450 
451     @Override
452     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
453         waitForFirstLoad();
454         loadStoppedAuthorities();
455         synchronized (mLock) {
456             return ProvidersAccess.getMatchingRoots(mRoots.values(), state);
457         }
458     }
459 
460     @Override
461     public Collection<RootInfo> getRootsForAuthorityBlocking(UserId userId, String authority) {
462         waitForFirstLoad();
463         UserAuthority userAuthority = new UserAuthority(userId, authority);
464         loadStoppedAuthority(userAuthority);
465         synchronized (mLock) {
466             final Collection<RootInfo> roots = mRoots.get(userAuthority);
467             return roots != null ? roots : Collections.<RootInfo>emptyList();
468         }
469     }
470 
471     @Override
472     public RootInfo getDefaultRootBlocking(State state) {
473         RootInfo root = ProvidersAccess.getDefaultRoot(getRootsBlocking(), state);
474         return root != null ? root : createOrGetRecentsRoot(UserId.CURRENT_USER);
475     }
476 
477     public void logCache() {
478         StringBuilder output = new StringBuilder();
479 
480         for (UserAuthority userAuthority : mObservedAuthoritiesDetails.keySet()) {
481             List<String> roots = new ArrayList<>();
482             Uri rootsUri = DocumentsContract.buildRootsUri(userAuthority.authority);
483             Bundle systemCache = userAuthority.userId.getContentResolver(mContext).getCache(
484                     rootsUri);
485             if (systemCache != null) {
486                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
487                 for (RootInfo root : cachedRoots) {
488                     roots.add(root.toDebugString());
489                 }
490             }
491 
492             output.append((output.length() == 0) ? "System cache: " : ", ");
493             output.append(userAuthority).append("=").append(roots);
494         }
495 
496         Log.i(TAG, output.toString());
497     }
498 
499     private class MultiProviderUpdateTask extends AsyncTask<Void, Void, Void> {
500         private final boolean mForceRefreshAll;
501         @Nullable
502         private final UserPackage mForceRefreshUserPackage;
503         @Nullable
504         private final Runnable mCallback;
505 
506         @GuardedBy("mLock")
507         private Multimap<UserAuthority, RootInfo> mLocalRoots = ArrayListMultimap.create();
508         @GuardedBy("mLock")
509         private HashSet<UserAuthority> mLocalStoppedAuthorities = new HashSet<>();
510 
511         /**
512          * Create task to update roots cache.
513          *
514          * @param forceRefreshAll         when true, all previously cached values for
515          *                                all packages should be ignored.
516          * @param forceRefreshUserPackage when non-null, all previously cached
517          *                                values for this specific user package should be ignored.
518          * @param callback                when non-null, it will be invoked after the task is
519          *                                executed.
520          */
521         MultiProviderUpdateTask(
522                 boolean forceRefreshAll,
523                 @Nullable UserPackage forceRefreshUserPackage,
524                 @Nullable Runnable callback) {
525             mForceRefreshAll = forceRefreshAll;
526             mForceRefreshUserPackage = forceRefreshUserPackage;
527             mCallback = callback;
528         }
529 
530         @Override
531         protected Void doInBackground(Void... params) {
532             if (!mMultiProviderUpdateTaskSemaphore.tryAcquire()) {
533                 // Abort, since previous update task is still running.
534                 return null;
535             }
536 
537             int previousPriority = Thread.currentThread().getPriority();
538             Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
539 
540             final long start = SystemClock.elapsedRealtime();
541 
542             List<UserId> userIds = new ArrayList<>(getUserIds());
543             for (UserId userId : userIds) {
544                 final RootInfo recents = createOrGetRecentsRoot(userId);
545                 synchronized (mLock) {
546                     mLocalRoots.put(new UserAuthority(recents.userId, recents.authority), recents);
547                 }
548             }
549 
550             List<SingleProviderUpdateTaskInfo> taskInfos = new ArrayList<>();
551             for (UserId userId : userIds) {
552                 final PackageManager pm = userId.getPackageManager(mContext);
553                 // Pick up provider with action string
554                 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
555                 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
556                 for (ResolveInfo info : providers) {
557                     ProviderInfo providerInfo = info.providerInfo;
558                     if (providerInfo.authority != null) {
559                         taskInfos.add(new SingleProviderUpdateTaskInfo(providerInfo, userId));
560                     }
561                 }
562             }
563 
564             if (!taskInfos.isEmpty()) {
565                 CountDownLatch updateTaskInternalCountDown = new CountDownLatch(taskInfos.size());
566                 ExecutorService executor = ASYNC_TASKS_THREAD_POOL;
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