RootScanner.java revision 47eb192b2704e27272ca94a95680cac40b6bff3f
1package com.android.mtp;
2
3import android.content.ContentResolver;
4import android.content.res.Resources;
5import android.database.sqlite.SQLiteException;
6import android.net.Uri;
7import android.os.Process;
8import android.provider.DocumentsContract;
9import android.util.Log;
10
11import java.io.IOException;
12
13final class RootScanner {
14    /**
15     * Polling interval in milliseconds used for first SHORT_POLLING_TIMES because it is more
16     * likely to add new root just after the device is added.
17     */
18    private final static long SHORT_POLLING_INTERVAL = 2000;
19
20    /**
21     * Polling interval in milliseconds for low priority polling, when changes are not expected.
22     */
23    private final static long LONG_POLLING_INTERVAL = 30 * 1000;
24
25    /**
26     * @see #SHORT_POLLING_INTERVAL
27     */
28    private final static long SHORT_POLLING_TIMES = 10;
29
30    final ContentResolver mResolver;
31    final Resources mResources;
32    final MtpManager mManager;
33    final MtpDatabase mDatabase;
34    boolean mClosed = false;
35    int mPollingCount;
36    Thread mBackgroundThread;
37
38    RootScanner(
39            ContentResolver resolver,
40            Resources resources,
41            MtpManager manager,
42            MtpDatabase database) {
43        mResolver = resolver;
44        mResources = resources;
45        mManager = manager;
46        mDatabase = database;
47    }
48
49    void notifyChange() {
50        final Uri uri =
51                DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY);
52        mResolver.notifyChange(uri, null, false);
53    }
54
55    /**
56     * Starts to check new changes right away.
57     * If the background thread has already gone, it restarts another background thread.
58     */
59    synchronized void scanNow() {
60        if (mClosed) {
61            return;
62        }
63        mPollingCount = 0;
64        if (mBackgroundThread == null) {
65            mBackgroundThread = new BackgroundLoaderThread();
66            mBackgroundThread.start();
67        } else {
68            notify();
69        }
70    }
71
72    void close() throws InterruptedException {
73        Thread thread;
74        synchronized (this) {
75            mClosed = true;
76            thread = mBackgroundThread;
77            if (mBackgroundThread == null) {
78                return;
79            }
80            notify();
81        }
82        thread.join();
83    }
84
85    private final class BackgroundLoaderThread extends Thread {
86        @Override
87        public void run() {
88            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
89            synchronized (RootScanner.this) {
90                while (!mClosed) {
91                    final int[] deviceIds = mManager.getOpenedDeviceIds();
92                    if (deviceIds.length == 0) {
93                        break;
94                    }
95                    boolean changed = false;
96                    for (int deviceId : deviceIds) {
97                        mDatabase.startAddingRootDocuments(deviceId);
98                        try {
99                            changed = mDatabase.putRootDocuments(
100                                    deviceId, mResources, mManager.getRoots(deviceId)) || changed;
101                        } catch (IOException|SQLiteException exp) {
102                            // The error may happen on the device. We would like to continue getting
103                            // roots for other devices.
104                            Log.e(MtpDocumentsProvider.TAG, exp.getMessage());
105                            continue;
106                        } finally {
107                            changed = mDatabase.stopAddingRootDocuments(deviceId) || changed;
108                        }
109                    }
110                    if (changed) {
111                        notifyChange();
112                    }
113                    mPollingCount++;
114                    try {
115                        // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is
116                        // more likely to add new root just after the device is added.
117                        // TODO: Use short interval only for a device that is just added.
118                        RootScanner.this.wait(
119                                mPollingCount > SHORT_POLLING_TIMES ?
120                                        LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL);
121                    } catch (InterruptedException exception) {
122                        break;
123                    }
124                }
125
126                mBackgroundThread = null;
127            }
128        }
129    }
130}
131