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 */
16package com.android.settings.dashboard;
17
18import android.app.Activity;
19import android.content.BroadcastReceiver;
20import android.content.ComponentName;
21import android.content.IntentFilter;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.HandlerThread;
25import android.os.Looper;
26import android.os.Message;
27import android.os.Process;
28import android.support.annotation.VisibleForTesting;
29import android.text.TextUtils;
30import android.util.ArrayMap;
31import android.util.ArraySet;
32import android.util.Log;
33
34import com.android.settings.SettingsActivity;
35import com.android.settings.overlay.FeatureFactory;
36import com.android.settingslib.drawer.DashboardCategory;
37import com.android.settingslib.drawer.Tile;
38
39import java.lang.reflect.Field;
40import java.util.List;
41
42public class SummaryLoader {
43    private static final boolean DEBUG = DashboardSummary.DEBUG;
44    private static final String TAG = "SummaryLoader";
45
46    public static final String SUMMARY_PROVIDER_FACTORY = "SUMMARY_PROVIDER_FACTORY";
47
48    private final Activity mActivity;
49    private final ArrayMap<SummaryProvider, ComponentName> mSummaryProviderMap = new ArrayMap<>();
50    private final ArrayMap<String, CharSequence> mSummaryTextMap = new ArrayMap<>();
51    private final DashboardFeatureProvider mDashboardFeatureProvider;
52    private final String mCategoryKey;
53
54    private final Worker mWorker;
55    private final Handler mHandler;
56    private final HandlerThread mWorkerThread;
57
58    private SummaryConsumer mSummaryConsumer;
59    private boolean mListening;
60    private boolean mWorkerListening;
61    private ArraySet<BroadcastReceiver> mReceivers = new ArraySet<>();
62
63    public SummaryLoader(Activity activity, List<DashboardCategory> categories) {
64        mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
65                .getDashboardFeatureProvider(activity);
66        mCategoryKey = null;
67        mHandler = new Handler();
68        mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
69        mWorkerThread.start();
70        mWorker = new Worker(mWorkerThread.getLooper());
71        mActivity = activity;
72        for (int i = 0; i < categories.size(); i++) {
73            List<Tile> tiles = categories.get(i).tiles;
74            for (int j = 0; j < tiles.size(); j++) {
75                Tile tile = tiles.get(j);
76                mWorker.obtainMessage(Worker.MSG_GET_PROVIDER, tile).sendToTarget();
77            }
78        }
79    }
80
81    public SummaryLoader(Activity activity, String categoryKey) {
82        mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
83                .getDashboardFeatureProvider(activity);
84        mCategoryKey = categoryKey;
85        mHandler = new Handler();
86        mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
87        mWorkerThread.start();
88        mWorker = new Worker(mWorkerThread.getLooper());
89        mActivity = activity;
90
91        final DashboardCategory category =
92                mDashboardFeatureProvider.getTilesForCategory(categoryKey);
93        if (category == null || category.tiles == null) {
94            return;
95        }
96
97        List<Tile> tiles = category.tiles;
98        for (Tile tile : tiles) {
99            mWorker.obtainMessage(Worker.MSG_GET_PROVIDER, tile).sendToTarget();
100        }
101    }
102
103    public void release() {
104        mWorkerThread.quitSafely();
105        // Make sure we aren't listening.
106        setListeningW(false);
107    }
108
109    public void setSummaryConsumer(SummaryConsumer summaryConsumer) {
110        mSummaryConsumer = summaryConsumer;
111    }
112
113    public void setSummary(SummaryProvider provider, final CharSequence summary) {
114        final ComponentName component = mSummaryProviderMap.get(provider);
115        mHandler.post(new Runnable() {
116            @Override
117            public void run() {
118
119                final Tile tile = getTileFromCategory(
120                        mDashboardFeatureProvider.getTilesForCategory(mCategoryKey), component);
121
122                if (tile == null) {
123                    if (DEBUG) {
124                        Log.d(TAG, "Can't find tile for " + component);
125                    }
126                    return;
127                }
128                if (DEBUG) {
129                    Log.d(TAG, "setSummary " + tile.title + " - " + summary);
130                }
131
132                updateSummaryIfNeeded(tile, summary);
133            }
134        });
135    }
136
137    @VisibleForTesting
138    void updateSummaryIfNeeded(Tile tile, CharSequence summary) {
139        if (TextUtils.equals(tile.summary, summary)) {
140            if (DEBUG) {
141                Log.d(TAG, "Summary doesn't change, skipping summary update for " + tile.title);
142            }
143            return;
144        }
145        mSummaryTextMap.put(mDashboardFeatureProvider.getDashboardKeyForTile(tile), summary);
146        tile.summary = summary;
147        if (mSummaryConsumer != null) {
148            mSummaryConsumer.notifySummaryChanged(tile);
149        } else {
150            if (DEBUG) {
151                Log.d(TAG, "SummaryConsumer is null, skipping summary update for "
152                        + tile.title);
153            }
154        }
155    }
156
157    /**
158     * Only call from the main thread.
159     */
160    public void setListening(boolean listening) {
161        if (mListening == listening) return;
162        mListening = listening;
163        // Unregister listeners immediately.
164        for (int i = 0; i < mReceivers.size(); i++) {
165            mActivity.unregisterReceiver(mReceivers.valueAt(i));
166        }
167        mReceivers.clear();
168        mWorker.removeMessages(Worker.MSG_SET_LISTENING);
169        mWorker.obtainMessage(Worker.MSG_SET_LISTENING, listening ? 1 : 0, 0).sendToTarget();
170    }
171
172    private SummaryProvider getSummaryProvider(Tile tile) {
173        if (!mActivity.getPackageName().equals(tile.intent.getComponent().getPackageName())) {
174            // Not within Settings, can't load Summary directly.
175            // TODO: Load summary indirectly.
176            return null;
177        }
178        Bundle metaData = getMetaData(tile);
179        if (metaData == null) {
180            if (DEBUG) Log.d(TAG, "No metadata specified for " + tile.intent.getComponent());
181            return null;
182        }
183        String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
184        if (clsName == null) {
185            if (DEBUG) Log.d(TAG, "No fragment specified for " + tile.intent.getComponent());
186            return null;
187        }
188        try {
189            Class<?> cls = Class.forName(clsName);
190            Field field = cls.getField(SUMMARY_PROVIDER_FACTORY);
191            SummaryProviderFactory factory = (SummaryProviderFactory) field.get(null);
192            return factory.createSummaryProvider(mActivity, this);
193        } catch (ClassNotFoundException e) {
194            if (DEBUG) Log.d(TAG, "Couldn't find " + clsName, e);
195        } catch (NoSuchFieldException e) {
196            if (DEBUG) Log.d(TAG, "Couldn't find " + SUMMARY_PROVIDER_FACTORY, e);
197        } catch (ClassCastException e) {
198            if (DEBUG) Log.d(TAG, "Couldn't cast " + SUMMARY_PROVIDER_FACTORY, e);
199        } catch (IllegalAccessException e) {
200            if (DEBUG) Log.d(TAG, "Couldn't get " + SUMMARY_PROVIDER_FACTORY, e);
201        }
202        return null;
203    }
204
205    private Bundle getMetaData(Tile tile) {
206        return tile.metaData;
207    }
208
209    /**
210     * Registers a receiver and automatically unregisters it when the activity is stopping.
211     * This ensures that the receivers are unregistered immediately, since most summary loader
212     * operations are asynchronous.
213     */
214    public void registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) {
215        mActivity.runOnUiThread(new Runnable() {
216            @Override
217            public void run() {
218                if (!mListening) {
219                    return;
220                }
221                mReceivers.add(receiver);
222                mActivity.registerReceiver(receiver, filter);
223            }
224        });
225    }
226
227    /**
228     * Updates all tile's summary to latest cached version. This is necessary to handle the case
229     * where category is updated after summary change.
230     */
231    public void updateSummaryToCache(DashboardCategory category) {
232        if (category == null) {
233            return;
234        }
235        for (Tile tile : category.tiles) {
236            final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
237            if (mSummaryTextMap.containsKey(key)) {
238                tile.summary = mSummaryTextMap.get(key);
239            }
240        }
241    }
242
243    private synchronized void setListeningW(boolean listening) {
244        if (mWorkerListening == listening) return;
245        mWorkerListening = listening;
246        if (DEBUG) Log.d(TAG, "Listening " + listening);
247        for (SummaryProvider p : mSummaryProviderMap.keySet()) {
248            try {
249                p.setListening(listening);
250            } catch (Exception e) {
251                Log.d(TAG, "Problem in setListening", e);
252            }
253        }
254    }
255
256    private synchronized void makeProviderW(Tile tile) {
257        SummaryProvider provider = getSummaryProvider(tile);
258        if (provider != null) {
259            if (DEBUG) Log.d(TAG, "Creating " + tile);
260            mSummaryProviderMap.put(provider, tile.intent.getComponent());
261        }
262    }
263
264    private Tile getTileFromCategory(DashboardCategory category, ComponentName component) {
265        if (category == null || category.tiles == null) {
266            return null;
267        }
268        final int tileCount = category.tiles.size();
269        for (int j = 0; j < tileCount; j++) {
270            final Tile tile = category.tiles.get(j);
271            if (component.equals(tile.intent.getComponent())) {
272                return tile;
273            }
274        }
275        return null;
276    }
277
278
279
280    public interface SummaryProvider {
281        void setListening(boolean listening);
282    }
283
284    public interface SummaryConsumer {
285        void notifySummaryChanged(Tile tile);
286    }
287
288    public interface SummaryProviderFactory {
289        SummaryProvider createSummaryProvider(Activity activity, SummaryLoader summaryLoader);
290    }
291
292    private class Worker extends Handler {
293        private static final int MSG_GET_PROVIDER = 1;
294        private static final int MSG_SET_LISTENING = 2;
295
296        public Worker(Looper looper) {
297            super(looper);
298        }
299
300        @Override
301        public void handleMessage(Message msg) {
302            switch (msg.what) {
303                case MSG_GET_PROVIDER:
304                    Tile tile = (Tile) msg.obj;
305                    makeProviderW(tile);
306                    break;
307                case MSG_SET_LISTENING:
308                    boolean listening = msg.arg1 != 0;
309                    setListeningW(listening);
310                    break;
311            }
312        }
313    }
314}
315