/*
* Copyright 2013 Google Inc.
*
* 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.google.android.apps.dashclock;
import android.app.backup.BackupManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import com.google.android.apps.dashclock.api.DashClockExtension;
import com.google.android.apps.dashclock.api.ExtensionData;
import com.google.android.apps.dashclock.api.host.ExtensionListing;
import com.google.android.apps.dashclock.gmail.GmailExtension;
import com.google.android.apps.dashclock.nextalarm.NextAlarmExtension;
import com.google.android.apps.dashclock.weather.WeatherExtension;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.android.apps.dashclock.LogUtils.LOGD;
import static com.google.android.apps.dashclock.LogUtils.LOGE;
import static com.google.android.apps.dashclock.LogUtils.LOGW;
/**
* A singleton class in charge of extension registration, activation (change in user-specified
* 'active' extensions), and data caching.
*/
public class ExtensionManager {
private static final String TAG = LogUtils.makeLogTag(ExtensionManager.class);
private final Context mApplicationContext;
private final List<ComponentName> mInternalActiveExtensions = new ArrayList<>();
private final Set<ExtensionWithData> mActiveExtensions = new HashSet<>();
private Map<ComponentName, ExtensionWithData> mExtensionInfoMap = new HashMap<>();
private List<OnChangeListener> mOnChangeListeners = new ArrayList<>();
private SharedPreferences mValuesPreferences;
private Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private static ExtensionManager sInstance;
private static final String PREF_ACTIVE_EXTENSIONS = "active_extensions";
private static final Class[] DEFAULT_EXTENSIONS = {
WeatherExtension.class,
GmailExtension.class,
NextAlarmExtension.class,
};
private SharedPreferences mDefaultPreferences;
private ExtensionManager(Context context) {
mApplicationContext = context.getApplicationContext();
mValuesPreferences = mApplicationContext.getSharedPreferences("extension_data", 0);
mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(mApplicationContext);
loadInternalActiveExtensionList();
}
public static ExtensionManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new ExtensionManager(context);
}
return sInstance;
}
/**
* De-activates active extensions that are unsupported or are no longer installed.
*/
public boolean cleanupExtensions() {
Set<ComponentName> availableExtensions = new HashSet<>();
for (ExtensionListing info : getAvailableExtensions()) {
// Ensure the extension protocol version is supported. If it isn't, don't allow its use.
if (!info.compatible()) {
LOGW(TAG, "Extension '" + info.title() + "' using unsupported protocol version "
+ info.protocolVersion() + ".");
continue;
}
availableExtensions.add(info.componentName());
}
boolean cleanupRequired = false;
Set<ComponentName> newActiveExtensions = new HashSet<>();
synchronized (mActiveExtensions) {
for (ExtensionWithData ewd : mActiveExtensions) {
if (availableExtensions.contains(ewd.listing.componentName())) {
newActiveExtensions.add(ewd.listing.componentName());
} else {
cleanupRequired = true;
}
}
}
if (cleanupRequired) {
setActiveExtensions(newActiveExtensions);
return true;
}
return false;
}
/**
* Replaces the set of active extensions with the given list.
*/
public void setActiveExtensions(Set<ComponentName> extensions) {
// Join external and internal extensions
Set<ComponentName> allExtensions = new HashSet<>(getInternalActiveExtensionNames());
for (ComponentName cn : extensions) {
if (!allExtensions.contains(cn)) {
allExtensions.add(cn);
}
}
Map<ComponentName, ExtensionListing> infos = new HashMap<>();
for (ExtensionListing info : getAvailableExtensions()) {
infos.put(info.componentName(), info);
}
Set<ComponentName> activeExtensionNames = getActiveExtensionNames();
if (activeExtensionNames.equals(allExtensions)) {
LOGD(TAG, "No change to list of active extensions.");
return;
}
// Clear cached data for any no-longer-active extensions.
for (ComponentName cn : activeExtensionNames) {
if (!allExtensions.contains(cn)) {
destroyExtensionData(cn);
}
}
// Set the new list of active extensions, loading cached data if necessary.
List<ExtensionWithData> newActiveExtensions = new ArrayList<>();
for (ComponentName cn : allExtensions) {
if (mExtensionInfoMap.containsKey(cn)) {
newActiveExtensions.add(mExtensionInfoMap.get(cn));
} else {
ExtensionWithData ewd = new ExtensionWithData();
ewd.listing = infos.get(cn);
if (ewd.listing == null) {
ewd.listing = new ExtensionListing();
ewd.listing.componentName(cn);
}
ewd.latestData = deserializeExtensionData(ewd.listing.componentName());
newActiveExtensions.add(ewd);
}
}
mExtensionInfoMap.clear();
for (ExtensionWithData ewd : newActiveExtensions) {
mExtensionInfoMap.put(ewd.listing.componentName(), ewd);
}
synchronized (mActiveExtensions) {
mActiveExtensions.clear();
mActiveExtensions.addAll(newActiveExtensions);
}
LOGD(TAG, "List of active extensions has changed.");
notifyOnChangeListeners(null);
}
/**
* Updates and caches the user-visible data for a given extension.
*/
public boolean updateExtensionData(ComponentName cn, ExtensionData data) {
data.clean();
ExtensionWithData ewd = mExtensionInfoMap.get(cn);
if (ewd != null && !ExtensionData.equals(ewd.latestData, data)) {
ewd.latestData = data;
serializeExtensionData(ewd.listing.componentName(), data);
notifyOnChangeListeners(ewd.listing.componentName());
return true;
}
return false;
}
private ExtensionData deserializeExtensionData(ComponentName componentName) {
ExtensionData extensionData = new ExtensionData();
String val = mValuesPreferences.getString(componentName.flattenToString(), "");
if (!TextUtils.isEmpty(val)) {
try {
extensionData.deserialize((JSONObject) new JSONTokener(val).nextValue());
} catch (JSONException e) {
LOGE(TAG, "Error loading extension data cache for " + componentName + ".",
e);
}
}
return extensionData;
}
private void serializeExtensionData(ComponentName componentName, ExtensionData extensionData) {
try {
mValuesPreferences.edit()
.putString(componentName.flattenToString(),
extensionData.serialize().toString())
.apply();
} catch (JSONException e) {
LOGE(TAG, "Error storing extension data cache for " + componentName + ".", e);
}
}
private void destroyExtensionData(ComponentName componentName) {
mValuesPreferences.edit()
.remove(componentName.flattenToString())
.apply();
}
public ExtensionWithData getExtensionWithData(ComponentName extension) {
return mExtensionInfoMap.get(extension);
}
public List<ExtensionWithData> getActiveExtensionsWithData() {
ArrayList<ExtensionWithData> activeExtensions;
synchronized (mActiveExtensions) {
activeExtensions = new ArrayList<>(mActiveExtensions);
}
return activeExtensions;
}
public List<ExtensionWithData> getInternalActiveExtensionsWithData() {
// Extract the data from the all active extension cache
List<ComponentName> internalActiveExtensions = getInternalActiveExtensionNames();
ArrayList<ExtensionWithData> activeExtensions = new ArrayList<>(
Arrays.asList(new ExtensionWithData[internalActiveExtensions.size()]));
synchronized (mActiveExtensions) {
for (ExtensionWithData ewd : mActiveExtensions) {
int pos = internalActiveExtensions.indexOf(ewd.listing.componentName());
if (pos >= 0) {
activeExtensions.set(pos, ewd);
}
}
}
// Clean any null/unset data
int count = activeExtensions.size();
for (int i = count - 1; i >= 0; i--) {
if (activeExtensions.get(i) == null) {
activeExtensions.remove(i);
}
}
return activeExtensions;
}
public Set<ComponentName> getActiveExtensionNames() {
Set<ComponentName> list = new HashSet<>();
for (ExtensionWithData ci : mActiveExtensions) {
list.add(ci.listing.componentName());
}
return list;
}
public List<ComponentName> getInternalActiveExtensionNames() {
return new ArrayList<>(mInternalActiveExtensions);
}
/**
* Returns a listing of all available (installed) extensions, including those that aren't
* world-readable.
*/
public List<ExtensionListing> getAvailableExtensions() {
List<ExtensionListing> availableExtensions = new ArrayList<>();
PackageManager pm = mApplicationContext.getPackageManager();
List<ResolveInfo> resolveInfos = pm.queryIntentServices(
new Intent(DashClockExtension.ACTION_EXTENSION), PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
ExtensionListing info = new ExtensionListing();
info.componentName(new ComponentName(resolveInfo.serviceInfo.packageName,
resolveInfo.serviceInfo.name));
info.title(resolveInfo.loadLabel(pm).toString());
Bundle metaData = resolveInfo.serviceInfo.metaData;
if (metaData != null) {
info.compatible(ExtensionHost.supportsProtocolVersion(
metaData.getInt("protocolVersion")));
info.worldReadable(metaData.getBoolean("worldReadable", false));
info.description(metaData.getString("description"));
String settingsActivity = metaData.getString("settingsActivity");
if (!TextUtils.isEmpty(settingsActivity)) {
info.settingsActivity(ComponentName.unflattenFromString(
resolveInfo.serviceInfo.packageName + "/" + settingsActivity));
}
}
info.icon(resolveInfo.getIconResource());
availableExtensions.add(info);
}
return availableExtensions;
}
private void loadInternalActiveExtensionList() {
List<ComponentName> activeExtensions = new ArrayList<>();
String extensions;
if (mDefaultPreferences.contains(PREF_ACTIVE_EXTENSIONS)) {
extensions = mDefaultPreferences.getString(PREF_ACTIVE_EXTENSIONS, "");
} else {
extensions = createDefaultExtensionList();
}
String[] componentNameStrings = extensions.split(",");
for (String componentNameString : componentNameStrings) {
if (TextUtils.isEmpty(componentNameString)) {
continue;
}
activeExtensions.add(ComponentName.unflattenFromString(componentNameString));
}
setInternalActiveExtensions(activeExtensions);
}
private String createDefaultExtensionList() {
StringBuilder sb = new StringBuilder();
for (Class cls : DEFAULT_EXTENSIONS) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(new ComponentName(mApplicationContext, cls).flattenToString());
}
return sb.toString();
}
/**
* Replaces the set of active extensions with the given list.
*/
public void setInternalActiveExtensions(List<ComponentName> extensions) {
StringBuilder sb = new StringBuilder();
for (ComponentName extension : extensions) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(extension.flattenToString());
}
mDefaultPreferences.edit()
.putString(PREF_ACTIVE_EXTENSIONS, sb.toString())
.apply();
new BackupManager(mApplicationContext).dataChanged();
mInternalActiveExtensions.clear();
mInternalActiveExtensions.addAll(extensions);
setActiveExtensions(getActiveExtensionNames());
}
public List<ExtensionWithData> getVisibleExtensionsWithData() {
ArrayList<ExtensionWithData> visibleExtensions = new ArrayList<>();
for (ExtensionWithData ewd : getInternalActiveExtensionsWithData()) {
if (ewd.latestData.visible()) {
visibleExtensions.add(ewd);
}
}
return visibleExtensions;
}
/**
* Registers a listener to be triggered when either the list of active extensions changes or an
* extension's data changes.
*/
public void addOnChangeListener(OnChangeListener onChangeListener) {
mOnChangeListeners.add(onChangeListener);
}
/**
* Removes a listener previously registered with {@link #addOnChangeListener}.
*/
public void removeOnChangeListener(OnChangeListener onChangeListener) {
mOnChangeListeners.remove(onChangeListener);
}
private void notifyOnChangeListeners(final ComponentName sourceExtension) {
mMainThreadHandler.post(new Runnable() {
@Override
public void run() {
for (OnChangeListener listener : mOnChangeListeners) {
listener.onExtensionsChanged(sourceExtension);
}
}
});
}
public interface OnChangeListener {
/**
* @param sourceExtension null if not related to any specific extension (e.g. list of
* extensions has changed).
*/
void onExtensionsChanged(ComponentName sourceExtension);
}
public static class ExtensionWithData {
public ExtensionListing listing;
public ExtensionData latestData;
}
}