/*
* 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.api.host;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import com.google.android.apps.dashclock.api.DashClockExtension;
import com.google.android.apps.dashclock.api.DashClockSignature;
import com.google.android.apps.dashclock.api.ExtensionData;
import com.google.android.apps.dashclock.api.internal.IDataConsumerHost;
import com.google.android.apps.dashclock.api.internal.IDataConsumerHostCallback;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static android.content.pm.PackageManager.NameNotFoundException;
/**
* Base class to be used by data consumer host implementations in order to get
* updates for installed extensions and their data.<br/>
* <p>
* Subclasses MUST call the {@link #listenTo(java.util.Set)} method to start listening
* for extension updates, and MUST call {@link #destroy()} when the instance is not
* used anymore.
* <p>
* Subclasses should implement {@link #onExtensionDataChanged(android.content.ComponentName)}
* in order to receive updates for the registered extensions passed to
* {@link #listenTo(java.util.Set)}.<br/>
* <p>
* Subclasses should implement {@link #onAvailableExtensionsChanged()} to get notifications
* of additions or removals of DashClock extensions installed on the device.<br/>
* <p>
* Subclasses should override {@link #onMultiplexerChangedDetected(boolean)}} to get notifications
* about changes in multiplexer service app availability.<br/>
* Subclasses are responsible for redirecting the user to the Play Store to download the official
* DashClock app. Subclasses should use {@link #getMultiplexerDownloadIntent()} to to get an
* intent to download the official app. An example UI can be found in the {@code example-host}
* project.
* <p>
* Lastly, there are a few <code><meta-data></code> elements that
* you should add to your service definition:
* <ul>
* <li><code>protocolVersion</code> (required): should be <strong>1</strong>.</li>
* </ul>
*/
public abstract class DashClockHost {
/**
* The required permission to bind to the {@link IDataConsumerHost} service
*/
public static final String BIND_DATA_CONSUMER_PERMISSION =
"com.google.android.apps.dashclock.permission.BIND_DATA_CONSUMER";
// The list of well-known multiplexer hosts
private static final ComponentName MULTIPLEXER_HOST_SERVICE =
new ComponentName("net.nurik.roman.dashclock",
"com.google.android.apps.dashclock.DashClockService");
private static final String ACTION_ASK_ENABLE_FORCE_WORLD_READABLE
= "com.google.android.apps.dashclock.action.ASK_ENABLE_FORCE_WORLD_READABLE";
/*
* PUBLIC API
*/
/**
* Exception thrown if the host multiplexer service is not available.
*/
public static class NoMultiplexerAvailableException extends RuntimeException {
private NoMultiplexerAvailableException(String message) {
super(message);
}
}
/**
* Returns whether a multiplexer host service app is present in the system.
*/
public static boolean isMultiplexerServicePresent(Context context) {
return getMultiplexerService(context) != null;
}
/**
* Return a list of packages that implement the {@link
* DashClockExtension#PERMISSION_READ_EXTENSION_DATA} permission and aren't DashClock
*/
public static List<String> getOtherAppsWithReadDataExtensionsPermission(Context context) {
List<String> installedApps = new ArrayList<>();
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// There is no problem with the PERMISSION_READ_EXTENSION_DATA if
// the api supports multiple apps defining the same permission (< Lollipop)
return installedApps;
}
PackageManager pm = context.getPackageManager();
List<ApplicationInfo> apps = pm.getInstalledApplications(PackageManager.GET_META_DATA);
for (ApplicationInfo ai : apps) {
try {
PackageInfo pi = pm.getPackageInfo(ai.packageName, PackageManager.GET_PERMISSIONS);
PermissionInfo[] perms = pi.permissions;
if (perms != null) {
for (PermissionInfo perm : perms) {
if (perm.name.equals(DashClockExtension.PERMISSION_READ_EXTENSION_DATA)) {
if (!ai.packageName.equals(MULTIPLEXER_HOST_SERVICE.getPackageName())) {
installedApps.add(ai.packageName);
break;
}
}
}
}
} catch (NameNotFoundException ex) {
// Ignore
}
}
return installedApps;
}
protected DashClockHost(Context context) throws SecurityException {
mContext = context;
mHandler = new Handler(mHandlerCallback);
mDataCache = new HashMap<>();
mAvailableExtensions = new ArrayList<>();
try {
if (!connect()) {
mHandler.sendEmptyMessageDelayed(MSG_RECONNECT, AUTO_RECONNECT_DELAY);
}
} catch (NoMultiplexerAvailableException | SecurityException ex) {
// Notify the implementation that the multiplexer isn't available
mHandler.obtainMessage(MSG_NOTIFY_MUX_NOT_AVAILABLE).sendToTarget();
}
}
/**
* Destroy this host instance, freeing its resources. After calling
* this, the host can no longer receive extension updates.
* This should be used in the onDestroy() method of the Activity or
* Service holding this host.
*/
public void destroy() {
mDestroyed = true;
mHandler.removeCallbacksAndMessages(null);
if (mService != null) {
try {
mService.listenTo(null, mCallback);
mContext.unbindService(mConnection);
} catch (RemoteException e) {
// ignored
}
}
}
/**
* Returns whether the user has expressly allowed non-world-readable
* extensions to be visible to all apps.
*
* @see #getEnableForceWorldReadabilityIntent()
*/
public boolean areNonWorldReadableExtensionsVisible() {
return mNonWorldReadableExtensionsVisible;
}
/**
* Returns an activity intent that can be called to ask the user to force
* world-readability enabled (that is, to set {@link #areNonWorldReadableExtensionsVisible()}
* to true.
*
* @see #areNonWorldReadableExtensionsVisible()
*/
public Intent getEnableForceWorldReadabilityIntent() {
return new Intent(ACTION_ASK_ENABLE_FORCE_WORLD_READABLE);
}
/**
* Get the list of known extensions.
*
* @return List of currently known extensions
*
* @see #onAvailableExtensionsChanged
*/
public List<ExtensionListing> getAvailableExtensions(boolean onlyWorldReadableExtensions) {
if (!onlyWorldReadableExtensions) {
return new ArrayList<>(mAvailableExtensions);
}
List<ExtensionListing> eis = new ArrayList<>(mAvailableExtensions);
int count = eis.size() - 1;
for (int i = count; i >= 0; i--) {
ExtensionListing ei = eis.get(i);
if (!ei.worldReadable()) {
eis.remove(i);
}
}
return eis;
}
/**
* Get the extension data for a given extension.
*
* @param extension Extension to get data for
* @return Known extension data or null
*
* @see #onExtensionDataChanged
*/
public ExtensionData getExtensionData(ComponentName extension) {
synchronized (mDataCache) {
return mDataCache.get(extension);
}
}
/**
* Update list of extensions to get data updates for.
*
* @param extensions List of extensions to monitor.
* @see #onExtensionDataChanged
*/
public void listenTo(Set<ComponentName> extensions) {
mListenedExtensions = extensions;
List<ComponentName> extensionList =
extensions == null ? null : new ArrayList<>(extensions);
try {
if (mService != null) {
mService.listenTo(extensionList, mCallback);
}
} catch (RemoteException e) {
// Ignore
}
}
/**
* Start the settings activity of a DashClock extension.
*
* If the extension doesn't define a settings activity, this is a no-op.
*
* @param extension The extension to show the settings for.
* @return Whether the operation was successful.
*/
public boolean startSettingsActivityForExtension(ExtensionListing extension) {
if (mService != null && extension.settingsActivity() != null) {
try {
mService.showExtensionSettings(extension.componentName(), mCallback);
return true;
} catch (RemoteException e) {
// Ignore
}
}
return false;
}
/**
* Request a manual update of extensions.
*
* @param extensions The list of extensions to update, or {@code null} to update all the
* active extensions for this host.
* @return Whether the operation was successful.
*/
public boolean requestExtensionUpdate(List<ComponentName> extensions) {
try {
if (mService != null) {
mService.requestExtensionUpdate(extensions, mCallback);
}
return true;
} catch (RemoteException ex) {
// Ignore
}
return false;
}
/**
* Called when the list of available extensions is updated.
*
* @see #getAvailableExtensions
*/
protected void onAvailableExtensionsChanged() {
}
/**
* Called when extension data for an extension is updated.
*
* @param extension Extension that was updated
*
* @see #getExtensionData
*/
protected void onExtensionDataChanged(ComponentName extension) {
}
/**
* Called when a multiplexer service package change was detected. That means
* a multiplexer service package was installed or removed.<br/>
* <br/>
* The default implementation ignores these events, so implementers of this
* class are encouraged to override this method and handle this event
* in a proper way.<br/>
* <br/>
* When there isn't an installed package providing a Multiplexer Service implementation,
* implementations <b>MUST</b> redirect the user to the official app in the Play Store.<br/>
* Implementations can obtain an {@link Intent} calling {@link #getMultiplexerDownloadIntent()}.
* <br/>
* An example UI to redirect the user can be found in the {@code example-host} project.
*
* @param available Indicates whether a multiplexer service is present.
*
* @see #isMultiplexerServicePresent(android.content.Context)
* @see #getMultiplexerDownloadIntent()
*/
protected void onMultiplexerChangedDetected(boolean available) {
}
/**
* Return an {@link android.content.Intent} reference to redirect the user
* to the Play Store to download the official DashClock app.<br/>
* Implementers should call to {@link Context#startActivity(android.content.Intent)}.
*
* @return The download {@link android.content.Intent} or {@code null} if the the multiplexer
* cannot be installed because another app that defines the {@link
* DashClockExtension#PERMISSION_READ_EXTENSION_DATA} is already installed.
*
* @see #getOtherAppsWithReadDataExtensionsPermission(android.content.Context)
*/
public final Intent getMultiplexerDownloadIntent() {
// First we need to check if there are other apps that declare the READ_EXTENSION_DATA
// permission. In that case, the update for DashClock or the installation of the mux
// will not work. Users MUST uninstall the app
List<String> apps = getOtherAppsWithReadDataExtensionsPermission(mContext);
if (!apps.isEmpty()) {
return null;
}
final String pkgName = MULTIPLEXER_HOST_SERVICE.getPackageName();
Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=" + pkgName);
return new Intent("android.intent.action.VIEW", uri);
}
/*
* INTERNAL IMPLEMENTATION
*/
private Context mContext;
private IDataConsumerHost mService;
private List<ExtensionListing> mAvailableExtensions;
private boolean mNonWorldReadableExtensionsVisible;
private final Map<ComponentName, ExtensionData> mDataCache;
private Set<ComponentName> mListenedExtensions;
private boolean mDestroyed;
// We assume that multiplexer is initially present. This makes sure
// onMultiplexerChangedDetected is called if the multiplexer isn't present initially
private boolean mIsMultiplexerPresent = true;
private final Handler mHandler;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IDataConsumerHost.Stub.asInterface(service);
try {
if (mListenedExtensions != null) {
listenTo(mListenedExtensions);
}
Message msg = mHandler.obtainMessage(MSG_NOTIFY_EXTENSION_LIST_CHANGE,
mService.areNonWorldReadableExtensionsVisible() ? 0 : 1,
0,
mService.getAvailableExtensions());
msg.sendToTarget();
} catch (RemoteException ex) {
mService = null;
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mNonWorldReadableExtensionsVisible = false;
mAvailableExtensions.clear();
mService = null;
if (!mDestroyed) {
mHandler.sendEmptyMessageDelayed(MSG_RECONNECT, AUTO_RECONNECT_DELAY);
}
}
};
public void handleMultiplexerPackageChanged() {
boolean isMultiplexerPresent = isMultiplexerServicePresent(mContext);
if (mIsMultiplexerPresent != isMultiplexerPresent) {
if (isMultiplexerPresent && mService == null) {
// Reconnect the service
Message msg = mHandler.obtainMessage(MSG_RECONNECT);
msg.sendToTarget();
} else if (!isMultiplexerPresent && mService != null) {
mContext.unbindService(mConnection);
}
onMultiplexerChangedDetected(isMultiplexerPresent);
mIsMultiplexerPresent = isMultiplexerPresent;
}
}
private final IDataConsumerHostCallback.Stub mCallback = new IDataConsumerHostCallback.Stub() {
@Override
public void notifyUpdate(ComponentName source, ExtensionData data) {
synchronized (mDataCache) {
mDataCache.put(source, data);
}
mHandler.obtainMessage(MSG_NOTIFY_DATA_CHANGE, source).sendToTarget();
}
@Override
public void notifyAvailableExtensionChanged(List<ExtensionListing> extensions,
boolean nonWorldReadableExtensionsVisible) {
mHandler.obtainMessage(MSG_NOTIFY_EXTENSION_LIST_CHANGE,
nonWorldReadableExtensionsVisible ? 0 : 1,
0,
extensions).sendToTarget();
}
};
private static final int AUTO_RECONNECT_DELAY = 5000;
private static final int MSG_NOTIFY_EXTENSION_LIST_CHANGE = 1;
private static final int MSG_NOTIFY_DATA_CHANGE = 2;
private static final int MSG_RECONNECT = 3;
private static final int MSG_NOTIFY_MUX_NOT_AVAILABLE = 4;
private final Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
@SuppressWarnings("unchecked")
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_NOTIFY_EXTENSION_LIST_CHANGE:
mAvailableExtensions.clear();
mAvailableExtensions.addAll((List<ExtensionListing>) msg.obj);
mNonWorldReadableExtensionsVisible = msg.arg1 == 0;
onAvailableExtensionsChanged();
return true;
case MSG_NOTIFY_DATA_CHANGE:
onExtensionDataChanged((ComponentName) msg.obj);
return true;
case MSG_RECONNECT:
try {
if (!connect()) {
mHandler.sendEmptyMessageDelayed(MSG_RECONNECT, AUTO_RECONNECT_DELAY);
}
} catch (NoMultiplexerAvailableException | SecurityException e) {
// Reconnect will not work, so stop here
}
return true;
case MSG_NOTIFY_MUX_NOT_AVAILABLE:
onMultiplexerChangedDetected(false);
mIsMultiplexerPresent = false;
return true;
}
return false;
}
};
/**
* Connects to the {@link IDataConsumerHost} multiplexer service.
*
* @return If connection to the multiplexer service was successful.
*/
private boolean connect() throws SecurityException, NoMultiplexerAvailableException {
ComponentName cn = getMultiplexerService(mContext);
if (cn == null) {
throw new NoMultiplexerAvailableException("Multiplexer service not installed");
}
// The multiplexer host service checks this, but we want to receive
// this prior to binding to the service.
enforcePermission(mContext, BIND_DATA_CONSUMER_PERMISSION);
// Instantiate the multiplexer service
Intent intent = new Intent();
intent.setComponent(cn);
return mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
/**
* Returns the name of a MultiplexerHostService present in the system or {@code null}
* if there isn't service available.
*/
private static ComponentName getMultiplexerService(Context context) {
PackageManager pm = context.getPackageManager();
boolean debuggable =
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
try {
PackageInfo pi = pm.getPackageInfo(MULTIPLEXER_HOST_SERVICE.getPackageName(),
PackageManager.GET_SIGNATURES | PackageManager.GET_SERVICES);
if (pi.applicationInfo.enabled) {
for (ServiceInfo si : pi.services) {
if (MULTIPLEXER_HOST_SERVICE.getClassName().equals(si.name) && si.enabled) {
if (debuggable) {
return MULTIPLEXER_HOST_SERVICE;
}
if (pi.signatures != null
&& pi.signatures.length == 1
&& DashClockSignature.SIGNATURE.equals(pi.signatures[0])) {
return MULTIPLEXER_HOST_SERVICE;
}
}
}
}
} catch (NameNotFoundException e) {
// ignored
}
return null;
}
private static void enforcePermission(Context context, String permission)
throws SecurityException {
// Check whether any of the caller's packages requests the expected permission
final PackageManager pm = context.getPackageManager();
final String packageName = context.getPackageName();
try {
PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
int count = pi.requestedPermissions != null ? pi.requestedPermissions.length : 0;
for (int i = 0; i < count; i++) {
if (pi.requestedPermissions[i].equals(permission)) {
// The caller has requested the permission
return;
}
}
} catch (NameNotFoundException e) {
// Ignore. Package wasn't found
}
throw new SecurityException("Caller didn't request the permission \"" + permission + "\"");
}
public static boolean isDashClockPresent(Context context) {
try {
// Check whether the DashClock multiplexer service is installed or not:
context.getPackageManager().getPackageInfo(
MULTIPLEXER_HOST_SERVICE.getPackageName(), 0);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
}