API for notification listener for Companioon apps

Test: 1. Trigger the confitrmation dialog.
Ensure it looks exactly like the one from settings.
2. Call an API without associating the appa first
Ensure exception is thrown with a message mentioning the need to associate 1st
Change-Id: I94d4116e1988db869ed445ae3fd018c50590e3f4
This commit is contained in:
Eugene Susla
2017-04-10 11:51:58 -07:00
parent 47dea3b16d
commit cf00adebec
16 changed files with 306 additions and 37 deletions

View File

@@ -8262,6 +8262,8 @@ package android.companion {
method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler);
method public void disassociate(java.lang.String);
method public java.util.List<java.lang.String> getAssociations();
method public boolean hasNotificationAccess(android.content.ComponentName);
method public void requestNotificationAccess(android.content.ComponentName);
field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE";
}

View File

@@ -8756,6 +8756,8 @@ package android.companion {
method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler);
method public void disassociate(java.lang.String);
method public java.util.List<java.lang.String> getAssociations();
method public boolean hasNotificationAccess(android.content.ComponentName);
method public void requestNotificationAccess(android.content.ComponentName);
field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE";
}

View File

@@ -8293,6 +8293,8 @@ package android.companion {
method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler);
method public void disassociate(java.lang.String);
method public java.util.List<java.lang.String> getAssociations();
method public boolean hasNotificationAccess(android.content.ComponentName);
method public void requestNotificationAccess(android.content.ComponentName);
field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE";
}

View File

@@ -22,11 +22,13 @@ import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService;
import android.util.Log;
import java.util.Collections;
@@ -195,22 +197,47 @@ public final class CompanionDeviceManager {
}
}
/** @hide */
public void requestNotificationAccess() {
/**
* Request notification access for the given component.
*
* The given component must follow the protocol specified in {@link NotificationListenerService}
*
* Only components from the same {@link ComponentName#getPackageName package} as the calling app
* are allowed.
*
* Your app must have an association with a device before calling this API
*/
public void requestNotificationAccess(ComponentName component) {
if (!checkFeaturePresent()) {
return;
}
//TODO implement
throw new UnsupportedOperationException("Not yet implemented");
try {
mService.requestNotificationAccess(component).send();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (PendingIntent.CanceledException e) {
throw new RuntimeException(e);
}
}
/** @hide */
public boolean haveNotificationAccess() {
/**
* Check whether the given component can access the notifications via a
* {@link NotificationListenerService}
*
* Your app must have an association with a device before calling this API
*
* @param component the name of the component
* @return whether the given component has the notification listener permission
*/
public boolean hasNotificationAccess(ComponentName component) {
if (!checkFeaturePresent()) {
return false;
}
//TODO implement
throw new UnsupportedOperationException("Not yet implemented");
try {
return mService.hasNotificationAccess(component);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private boolean checkFeaturePresent() {

View File

@@ -16,8 +16,10 @@
package android.companion;
import android.app.PendingIntent;
import android.companion.IFindDeviceCallback;
import android.companion.AssociationRequest;
import android.content.ComponentName;
/**
* Interface for communication with the core companion device manager service.
@@ -32,7 +34,6 @@ interface ICompanionDeviceManager {
List<String> getAssociations(String callingPackage, int userId);
void disassociate(String deviceMacAddress, String callingPackage);
//TODO add these
// boolean haveNotificationAccess(String packageName);
// oneway void requestNotificationAccess(String packageName);
boolean hasNotificationAccess(in ComponentName component);
PendingIntent requestNotificationAccess(in ComponentName component);
}

View File

@@ -16,9 +16,15 @@
package android.os;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.Slog;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.FunctionalUtils;
import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
import libcore.io.IoUtils;
import java.io.FileDescriptor;
@@ -26,7 +32,6 @@ import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.lang.reflect.Modifier;
import java.util.function.Supplier;
/**
* Base class for a remotable object, the core part of a lightweight
@@ -251,14 +256,23 @@ public class Binder implements IBinder {
* Convenience method for running the provided action enclosed in
* {@link #clearCallingIdentity}/{@link #restoreCallingIdentity}
*
* Any exception thrown by the given action will be caught and rethrown after the call to
* {@link #restoreCallingIdentity}
*
* @hide
*/
public static final void withCleanCallingIdentity(Runnable action) {
public static final void withCleanCallingIdentity(ThrowingRunnable action) {
long callingIdentity = clearCallingIdentity();
Throwable throwableToPropagate = null;
try {
action.run();
} catch (Throwable throwable) {
throwableToPropagate = throwable;
} finally {
restoreCallingIdentity(callingIdentity);
if (throwableToPropagate != null) {
throw ExceptionUtils.propagate(throwableToPropagate);
}
}
}
@@ -266,14 +280,24 @@ public class Binder implements IBinder {
* Convenience method for running the provided action enclosed in
* {@link #clearCallingIdentity}/{@link #restoreCallingIdentity} returning the result
*
* Any exception thrown by the given action will be caught and rethrown after the call to
* {@link #restoreCallingIdentity}
*
* @hide
*/
public static final <T> T withCleanCallingIdentity(Supplier<T> action) {
public static final <T> T withCleanCallingIdentity(ThrowingSupplier<T> action) {
long callingIdentity = clearCallingIdentity();
Throwable throwableToPropagate = null;
try {
return action.get();
} catch (Throwable throwable) {
throwableToPropagate = throwable;
return null; // overridden by throwing in finally block
} finally {
restoreCallingIdentity(callingIdentity);
if (throwableToPropagate != null) {
throw ExceptionUtils.propagate(throwableToPropagate);
}
}
}

View File

@@ -23,6 +23,7 @@ import android.text.TextUtils;
import com.android.internal.util.ArrayUtils;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.function.Function;
@@ -80,6 +81,12 @@ public class SettingsStringUtil {
return s;
}
public static String addAll(String delimitedElements, Collection<String> elements) {
final ColonDelimitedSet<String> set
= new ColonDelimitedSet.OfStrings(delimitedElements);
return set.addAll(elements) ? set.toString() : delimitedElements;
}
public static String add(String delimitedElements, String element) {
final ColonDelimitedSet<String> set
= new ColonDelimitedSet.OfStrings(delimitedElements);

View File

@@ -67,10 +67,15 @@ public abstract class AlertActivity extends Activity implements DialogInterface
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
event.setClassName(Dialog.class.getName());
event.setPackageName(getPackageName());
return dispatchPopulateAccessibilityEvent(this, event);
}
ViewGroup.LayoutParams params = getWindow().getAttributes();
public static boolean dispatchPopulateAccessibilityEvent(Activity act,
AccessibilityEvent event) {
event.setClassName(Dialog.class.getName());
event.setPackageName(act.getPackageName());
ViewGroup.LayoutParams params = act.getWindow().getAttributes();
boolean isFullScreen = (params.width == ViewGroup.LayoutParams.MATCH_PARENT) &&
(params.height == ViewGroup.LayoutParams.MATCH_PARENT);
event.setFullScreen(isFullScreen);
@@ -86,8 +91,7 @@ public abstract class AlertActivity extends Activity implements DialogInterface
* @see #mAlertParams
*/
protected void setupAlert() {
mAlertParams.apply(mAlert);
mAlert.installContent();
mAlert.installContent(mAlertParams);
}
@Override

View File

@@ -247,6 +247,11 @@ public class AlertController {
return false;
}
public void installContent(AlertParams params) {
params.apply(this);
installContent();
}
public void installContent() {
int contentView = selectContentView();
mWindow.setContentView(contentView);

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2017 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.internal.notification;
import android.content.ComponentName;
import android.content.Intent;
public final class NotificationAccessConfirmationActivityContract {
private static final ComponentName COMPONENT_NAME = new ComponentName(
"com.android.settings",
"com.android.settings.notification.NotificationAccessConfirmationActivity");
public static final String EXTRA_USER_ID = "user_id";
public static final String EXTRA_COMPONENT_NAME = "component_name";
public static final String EXTRA_PACKAGE_TITLE = "package_title";
public static Intent launcherIntent(int userId, ComponentName component, String packageTitle) {
return new Intent()
.setComponent(COMPONENT_NAME)
.putExtra(EXTRA_USER_ID, userId)
.putExtra(EXTRA_COMPONENT_NAME, component)
.putExtra(EXTRA_PACKAGE_TITLE, packageTitle);
}
}

View File

@@ -16,6 +16,8 @@
package com.android.internal.util;
import static com.android.internal.util.ArrayUtils.isEmpty;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -64,7 +66,7 @@ public class CollectionUtils {
*/
public static @NonNull <I, O> List<O> map(@Nullable List<I> cur,
Function<? super I, ? extends O> f) {
if (cur == null || cur.isEmpty()) return Collections.emptyList();
if (isEmpty(cur)) return Collections.emptyList();
final ArrayList<O> result = new ArrayList<>();
for (int i = 0; i < cur.size(); i++) {
result.add(f.apply(cur.get(i)));
@@ -72,6 +74,30 @@ public class CollectionUtils {
return result;
}
/**
* {@link #map(List, Function)} + {@link #filter(List, java.util.function.Predicate)}
*
* Calling this is equivalent (but more memory efficient) to:
*
* {@code
* filter(
* map(cur, f),
* i -> { i != null })
* }
*/
public static @NonNull <I, O> List<O> mapNotNull(@Nullable List<I> cur,
Function<? super I, ? extends O> f) {
if (isEmpty(cur)) return Collections.emptyList();
final ArrayList<O> result = new ArrayList<>();
for (int i = 0; i < cur.size(); i++) {
O transformed = f.apply(cur.get(i));
if (transformed != null) {
result.add(transformed);
}
}
return result;
}
/**
* Returns the given list, or an immutable empty list if the provided list is null
*
@@ -94,7 +120,7 @@ public class CollectionUtils {
* Returns the elements of the given list that are of type {@code c}
*/
public static @NonNull <T> List<T> filter(@Nullable List<?> list, Class<T> c) {
if (ArrayUtils.isEmpty(list)) return Collections.emptyList();
if (isEmpty(list)) return Collections.emptyList();
ArrayList<T> result = null;
for (int i = 0; i < list.size(); i++) {
final Object item = list.get(i);
@@ -120,7 +146,7 @@ public class CollectionUtils {
*/
public static @Nullable <T> T find(@Nullable List<T> items,
java.util.function.Predicate<T> predicate) {
if (ArrayUtils.isEmpty(items)) return null;
if (isEmpty(items)) return null;
for (int i = 0; i < items.size(); i++) {
final T item = items.get(i);
if (predicate.test(item)) return item;
@@ -145,11 +171,17 @@ public class CollectionUtils {
* {@link Collections#emptyList}
*/
public static @NonNull <T> List<T> remove(@Nullable List<T> cur, T val) {
if (cur == null || cur == Collections.emptyList()) {
return Collections.emptyList();
if (isEmpty(cur)) {
return emptyIfNull(cur);
}
cur.remove(val);
return cur;
}
/**
* @return a list that will not be affected by mutations to the given original list.
*/
public static @NonNull <T> List<T> copyOf(@Nullable List<T> cur) {
return isEmpty(cur) ? Collections.emptyList() : new ArrayList<>(cur);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2017 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.internal.util;
import java.util.function.Supplier;
/**
* Utilities specific to functional programming
*/
public class FunctionalUtils {
private FunctionalUtils() {}
/**
* An equivalent of {@link Runnable} that allows throwing checked exceptions
*
* This can be used to specify a lambda argument without forcing all the checked exceptions
* to be handled within it
*/
@FunctionalInterface
public interface ThrowingRunnable {
void run() throws Exception;
}
/**
* An equivalent of {@link Supplier} that allows throwing checked exceptions
*
* This can be used to specify a lambda argument without forcing all the checked exceptions
* to be handled within it
*/
@FunctionalInterface
public interface ThrowingSupplier<T> {
T get() throws Exception;
}
}

View File

@@ -48,6 +48,23 @@ public class Preconditions {
}
}
/**
* Ensures that an expression checking an argument is true.
*
* @param expression the expression to check
* @param messageTemplate a printf-style message template to use if the check fails; will
* be converted to a string using {@link String#format(String, Object...)}
* @param messageArgs arguments for {@code messageTemplate}
* @throws IllegalArgumentException if {@code expression} is false
*/
public static void checkArgument(boolean expression,
final String messageTemplate,
final Object... messageArgs) {
if (!expression) {
throw new IllegalArgumentException(String.format(messageTemplate, messageArgs));
}
}
/**
* Ensures that an string reference passed as a parameter to the calling
* method is not empty.

View File

@@ -3556,6 +3556,11 @@
android:process=":ui">
</activity>
<activity android:name="com.android.settings.notification.NotificationAccessConfirmationActivity"
android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.Alert"
android:excludeFromRecents="true">
</activity>
<receiver android:name="com.android.server.BootReceiver"
android:systemUserOnly="true">
<intent-filter android:priority="1000">

View File

@@ -19,6 +19,9 @@
<!-- Private symbols that we need to reference from framework code. See
frameworks/base/core/res/MakeJavaSymbols.sed for how to easily generate
this.
Can be referenced in java code as: com.android.internal.R.<type>.<name>
and in layout xml as: "@*android:<type>/<name>"
-->
<java-symbol type="id" name="account_name" />
<java-symbol type="id" name="account_row_icon" />

View File

@@ -20,10 +20,12 @@ package com.android.server.print;
import static com.android.internal.util.CollectionUtils.size;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
import android.Manifest;
import android.annotation.CheckResult;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
import android.companion.ICompanionDeviceDiscoveryService;
@@ -47,13 +49,18 @@ import android.os.Parcel;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.SettingsStringUtil.ComponentNameSet;
import android.text.BidiFormatter;
import android.util.AtomicFile;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.Slog;
import android.util.Xml;
import com.android.internal.app.IAppOpsService;
import com.android.internal.content.PackageMonitor;
import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.server.FgThread;
@@ -80,6 +87,7 @@ import java.util.function.Function;
//TODO schedule stopScan on activity destroy(except if configuration change)
//TODO on associate called again after configuration change -> replace old callback with new
//TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example)
//TODO check user-feature present in manifest on API calls
/** @hide */
public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
@@ -217,6 +225,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
a -> a.deviceAddress);
}
//TODO also revoke notification access
@Override
public void disassociate(String deviceMacAddress, String callingPackage)
throws RemoteException {
@@ -237,11 +246,49 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
checkArgument(getCallingUserId() == userId,
"Must be called by either same user or system");
mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);
}
@Override
public PendingIntent requestNotificationAccess(ComponentName component)
throws RemoteException {
String callingPackage = component.getPackageName();
checkCanCallNotificationApi(callingPackage);
int userId = getCallingUserId();
String packageTitle = BidiFormatter.getInstance().unicodeWrap(
getPackageInfo(callingPackage, userId)
.applicationInfo
.loadSafeLabel(getContext().getPackageManager())
.toString());
long identity = Binder.clearCallingIdentity();
try {
return PendingIntent.getActivity(getContext(),
0 /* request code */,
NotificationAccessConfirmationActivityContract.launcherIntent(
userId, component, packageTitle),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT
| PendingIntent.FLAG_CANCEL_CURRENT);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
checkCanCallNotificationApi(component.getPackageName());
String setting = Settings.Secure.getString(getContext().getContentResolver(),
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
return new ComponentNameSet(setting).contains(component);
}
private void checkCanCallNotificationApi(String callingPackage) throws RemoteException {
checkCallerIsSystemOr(callingPackage);
checkState(!ArrayUtils.isEmpty(readAllAssociations(getCallingUserId(), callingPackage)),
"App must have an association before calling this API");
}
}
private int getCallingUserId() {
return UserHandle.getUserId(Binder.getCallingUid());
}
@@ -290,6 +337,17 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() {
return new ICompanionDeviceDiscoveryServiceCallback.Stub() {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
try {
return super.onTransact(code, data, reply, flags);
} catch (Throwable e) {
Slog.e(LOG_TAG, "Error during IPC", e);
throw ExceptionUtils.propagate(e, RemoteException.class);
}
}
@Override
public void onDeviceSelected(String packageName, int userId, String deviceAddress) {
updateSpecialAccessPermissionForAssociatedPackage(packageName, userId);
@@ -301,7 +359,6 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
public void onDeviceSelectionCancel() {
cleanup();
}
};
}
@@ -351,8 +408,13 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
}
private void recordAssociation(String priviledgedPackage, String deviceAddress) {
if (DEBUG) {
Log.i(LOG_TAG, "recordAssociation(priviledgedPackage = " + priviledgedPackage
+ ", deviceAddress = " + deviceAddress + ")");
}
int userId = getCallingUserId();
updateAssociations(associations -> CollectionUtils.add(associations,
new Association(getCallingUserId(), deviceAddress, priviledgedPackage)));
new Association(userId, deviceAddress, priviledgedPackage)));
}
private void updateAssociations(Function<List<Association>, List<Association>> update) {
@@ -364,7 +426,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
final AtomicFile file = getStorageFileForUser(userId);
synchronized (file) {
List<Association> associations = readAllAssociations(userId);
final ArrayList<Association> old = new ArrayList<>(associations);
final List<Association> old = CollectionUtils.copyOf(associations);
associations = update.apply(associations);
if (size(old) == size(associations)) return;
@@ -394,15 +456,6 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
});
}
//TODO Show dialog before recording notification access
// final SettingStringHelper setting =
// new SettingStringHelper(
// getContext().getContentResolver(),
// Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
// getUserId());
// setting.write(ColonDelimitedSet.OfStrings.add(setting.read(), priviledgedPackage));
}
private AtomicFile getStorageFileForUser(int uid) {