platform-packages-apps-Settings / src / com / android / settings / dashboard / SummaryLoader.java
SummaryLoader.java
Raw
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.settings.dashboard;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

import com.android.settings.SettingsActivity;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.utils.ThreadUtils;

import java.lang.reflect.Field;
import java.util.List;

public class SummaryLoader {
    private static final boolean DEBUG = DashboardSummary.DEBUG;
    private static final String TAG = "SummaryLoader";

    public static final String SUMMARY_PROVIDER_FACTORY = "SUMMARY_PROVIDER_FACTORY";

    private final Activity mActivity;
    private final ArrayMap<SummaryProvider, ComponentName> mSummaryProviderMap = new ArrayMap<>();
    private final ArrayMap<String, CharSequence> mSummaryTextMap = new ArrayMap<>();
    private final DashboardFeatureProvider mDashboardFeatureProvider;
    private final String mCategoryKey;

    private final Worker mWorker;
    private final HandlerThread mWorkerThread;

    private SummaryConsumer mSummaryConsumer;
    private boolean mListening;
    private boolean mWorkerListening;
    private ArraySet<BroadcastReceiver> mReceivers = new ArraySet<>();

    public SummaryLoader(Activity activity, String categoryKey) {
        mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
                .getDashboardFeatureProvider(activity);
        mCategoryKey = categoryKey;
        mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
        mWorkerThread.start();
        mWorker = new Worker(mWorkerThread.getLooper());
        mActivity = activity;
    }

    public void release() {
        mWorkerThread.quitSafely();
        // Make sure we aren't listening.
        setListeningW(false);
    }

    public void setSummaryConsumer(SummaryConsumer summaryConsumer) {
        mSummaryConsumer = summaryConsumer;
    }

    public void setSummary(SummaryProvider provider, final CharSequence summary) {
        final ComponentName component = mSummaryProviderMap.get(provider);
        ThreadUtils.postOnMainThread(() -> {

            final Tile tile = getTileFromCategory(
                    mDashboardFeatureProvider.getTilesForCategory(mCategoryKey), component);

            if (tile == null) {
                if (DEBUG) {
                    Log.d(TAG, "Can't find tile for " + component);
                }
                return;
            }
            if (DEBUG) {
                Log.d(TAG, "setSummary " + tile.title + " - " + summary);
            }

            updateSummaryIfNeeded(tile, summary);
        });
    }

    @VisibleForTesting
    void updateSummaryIfNeeded(Tile tile, CharSequence summary) {
        if (TextUtils.equals(tile.summary, summary)) {
            if (DEBUG) {
                Log.d(TAG, "Summary doesn't change, skipping summary update for " + tile.title);
            }
            return;
        }
        mSummaryTextMap.put(mDashboardFeatureProvider.getDashboardKeyForTile(tile), summary);
        tile.summary = summary;
        if (mSummaryConsumer != null) {
            mSummaryConsumer.notifySummaryChanged(tile);
        } else {
            if (DEBUG) {
                Log.d(TAG, "SummaryConsumer is null, skipping summary update for "
                        + tile.title);
            }
        }
    }

    /**
     * Only call from the main thread.
     */
    public void setListening(boolean listening) {
        if (mListening == listening) {
            return;
        }
        mListening = listening;
        // Unregister listeners immediately.
        for (int i = 0; i < mReceivers.size(); i++) {
            mActivity.unregisterReceiver(mReceivers.valueAt(i));
        }
        mReceivers.clear();

        mWorker.removeMessages(Worker.MSG_SET_LISTENING);
        if (!listening) {
            // Stop listen
            mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 0 /* listening */).sendToTarget();
        } else {
            // Start listen
            if (mSummaryProviderMap.isEmpty()) {
                // Category not initialized yet, init before starting to listen
                if (!mWorker.hasMessages(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING)) {
                    mWorker.sendEmptyMessage(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING);
                }
            } else {
                // Category already initialized, start listening immediately
                mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 1 /* listening */).sendToTarget();
            }
        }
    }

    private SummaryProvider getSummaryProvider(Tile tile) {
        if (!mActivity.getPackageName().equals(tile.intent.getComponent().getPackageName())) {
            // Not within Settings, can't load Summary directly.
            // TODO: Load summary indirectly.
            return null;
        }
        Bundle metaData = getMetaData(tile);
        if (metaData == null) {
            if (DEBUG) Log.d(TAG, "No metadata specified for " + tile.intent.getComponent());
            return null;
        }
        String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
        if (clsName == null) {
            if (DEBUG) Log.d(TAG, "No fragment specified for " + tile.intent.getComponent());
            return null;
        }
        try {
            Class<?> cls = Class.forName(clsName);
            Field field = cls.getField(SUMMARY_PROVIDER_FACTORY);
            SummaryProviderFactory factory = (SummaryProviderFactory) field.get(null);
            return factory.createSummaryProvider(mActivity, this);
        } catch (ClassNotFoundException e) {
            if (DEBUG) Log.d(TAG, "Couldn't find " + clsName, e);
        } catch (NoSuchFieldException e) {
            if (DEBUG) Log.d(TAG, "Couldn't find " + SUMMARY_PROVIDER_FACTORY, e);
        } catch (ClassCastException e) {
            if (DEBUG) Log.d(TAG, "Couldn't cast " + SUMMARY_PROVIDER_FACTORY, e);
        } catch (IllegalAccessException e) {
            if (DEBUG) Log.d(TAG, "Couldn't get " + SUMMARY_PROVIDER_FACTORY, e);
        }
        return null;
    }

    private Bundle getMetaData(Tile tile) {
        return tile.metaData;
    }

    /**
     * Registers a receiver and automatically unregisters it when the activity is stopping.
     * This ensures that the receivers are unregistered immediately, since most summary loader
     * operations are asynchronous.
     */
    public void registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (!mListening) {
                    return;
                }
                mReceivers.add(receiver);
                mActivity.registerReceiver(receiver, filter);
            }
        });
    }

    /**
     * Updates all tile's summary to latest cached version. This is necessary to handle the case
     * where category is updated after summary change.
     */
    public void updateSummaryToCache(DashboardCategory category) {
        if (category == null) {
            return;
        }
        for (Tile tile : category.getTiles()) {
            final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
            if (mSummaryTextMap.containsKey(key)) {
                tile.summary = mSummaryTextMap.get(key);
            }
        }
    }

    private synchronized void setListeningW(boolean listening) {
        if (mWorkerListening == listening) {
            return;
        }
        mWorkerListening = listening;
        if (DEBUG) {
            Log.d(TAG, "Listening " + listening);
        }
        for (SummaryProvider p : mSummaryProviderMap.keySet()) {
            try {
                p.setListening(listening);
            } catch (Exception e) {
                Log.d(TAG, "Problem in setListening", e);
            }
        }
    }

    private synchronized void makeProviderW(Tile tile) {
        SummaryProvider provider = getSummaryProvider(tile);
        if (provider != null) {
            if (DEBUG) Log.d(TAG, "Creating " + tile);
            mSummaryProviderMap.put(provider, tile.intent.getComponent());
        }
    }

    private Tile getTileFromCategory(DashboardCategory category, ComponentName component) {
        if (category == null || category.getTilesCount() == 0) {
            return null;
        }
        final List<Tile> tiles = category.getTiles();
        final int tileCount = tiles.size();
        for (int j = 0; j < tileCount; j++) {
            final Tile tile = tiles.get(j);
            if (component.equals(tile.intent.getComponent())) {
                return tile;
            }
        }
        return null;
    }


    public interface SummaryProvider {
        void setListening(boolean listening);
    }

    public interface SummaryConsumer {
        void notifySummaryChanged(Tile tile);
    }

    public interface SummaryProviderFactory {
        SummaryProvider createSummaryProvider(Activity activity, SummaryLoader summaryLoader);
    }

    private class Worker extends Handler {
        private static final int MSG_GET_CATEGORY_TILES_AND_SET_LISTENING = 1;
        private static final int MSG_GET_PROVIDER = 2;
        private static final int MSG_SET_LISTENING = 3;

        public Worker(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_GET_CATEGORY_TILES_AND_SET_LISTENING:
                    final DashboardCategory category =
                            mDashboardFeatureProvider.getTilesForCategory(mCategoryKey);
                    if (category == null || category.getTilesCount() == 0) {
                        return;
                    }
                    final List<Tile> tiles = category.getTiles();
                    for (Tile tile : tiles) {
                        makeProviderW(tile);
                    }
                    setListeningW(true);
                    break;
                case MSG_GET_PROVIDER:
                    Tile tile = (Tile) msg.obj;
                    makeProviderW(tile);
                    break;
                case MSG_SET_LISTENING:
                    boolean listening = msg.obj != null && msg.obj.equals(1);
                    setListeningW(listening);
                    break;
            }
        }
    }
}