| /** | |
| * Copyright (c) 2014, 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.server.notification; | |
| import android.app.INotificationManager; | |
| import android.app.NotificationManager; | |
| import android.content.ComponentName; | |
| import android.content.Context; | |
| import android.content.pm.IPackageManager; | |
| import android.net.Uri; | |
| import android.os.IBinder; | |
| import android.os.IInterface; | |
| import android.os.RemoteException; | |
| import android.os.UserHandle; | |
| import android.provider.Settings; | |
| import android.service.notification.Condition; | |
| import android.service.notification.ConditionProviderService; | |
| import android.service.notification.IConditionProvider; | |
| import android.util.ArrayMap; | |
| import android.util.ArraySet; | |
| import android.util.Slog; | |
| import com.android.internal.R; | |
| import com.android.internal.annotations.VisibleForTesting; | |
| import com.android.server.notification.NotificationManagerService.DumpFilter; | |
| import java.io.PrintWriter; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| public class ConditionProviders extends ManagedServices { | |
| @VisibleForTesting | |
| static final String TAG_ENABLED_DND_APPS = "dnd_apps"; | |
| private final ArrayList<ConditionRecord> mRecords = new ArrayList<>(); | |
| private final ArraySet<String> mSystemConditionProviderNames; | |
| private final ArraySet<SystemConditionProviderService> mSystemConditionProviders | |
| = new ArraySet<>(); | |
| private Callback mCallback; | |
| public ConditionProviders(Context context, UserProfiles userProfiles, IPackageManager pm) { | |
| super(context, new Object(), userProfiles, pm); | |
| mSystemConditionProviderNames = safeSet(PropConfig.getStringArray(mContext, | |
| "system.condition.providers", | |
| R.array.config_system_condition_providers)); | |
| mApprovalLevel = APPROVAL_BY_PACKAGE; | |
| } | |
| public void setCallback(Callback callback) { | |
| mCallback = callback; | |
| } | |
| public boolean isSystemProviderEnabled(String path) { | |
| return mSystemConditionProviderNames.contains(path); | |
| } | |
| public void addSystemProvider(SystemConditionProviderService service) { | |
| mSystemConditionProviders.add(service); | |
| service.attachBase(mContext); | |
| registerService(service.asInterface(), service.getComponent(), UserHandle.USER_SYSTEM); | |
| } | |
| public Iterable<SystemConditionProviderService> getSystemProviders() { | |
| return mSystemConditionProviders; | |
| } | |
| @Override | |
| protected Config getConfig() { | |
| final Config c = new Config(); | |
| c.caption = "condition provider"; | |
| c.serviceInterface = ConditionProviderService.SERVICE_INTERFACE; | |
| c.secureSettingName = Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES; | |
| c.xmlTag = TAG_ENABLED_DND_APPS; | |
| c.secondarySettingName = Settings.Secure.ENABLED_NOTIFICATION_LISTENERS; | |
| c.bindPermission = android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE; | |
| c.settingsAction = Settings.ACTION_CONDITION_PROVIDER_SETTINGS; | |
| c.clientLabel = R.string.condition_provider_service_binding_label; | |
| return c; | |
| } | |
| @Override | |
| public void dump(PrintWriter pw, DumpFilter filter) { | |
| super.dump(pw, filter); | |
| synchronized(mMutex) { | |
| pw.print(" mRecords("); pw.print(mRecords.size()); pw.println("):"); | |
| for (int i = 0; i < mRecords.size(); i++) { | |
| final ConditionRecord r = mRecords.get(i); | |
| if (filter != null && !filter.matches(r.component)) continue; | |
| pw.print(" "); pw.println(r); | |
| final String countdownDesc = CountdownConditionProvider.tryParseDescription(r.id); | |
| if (countdownDesc != null) { | |
| pw.print(" ("); pw.print(countdownDesc); pw.println(")"); | |
| } | |
| } | |
| } | |
| pw.print(" mSystemConditionProviders: "); pw.println(mSystemConditionProviderNames); | |
| for (int i = 0; i < mSystemConditionProviders.size(); i++) { | |
| mSystemConditionProviders.valueAt(i).dump(pw, filter); | |
| } | |
| } | |
| @Override | |
| protected IInterface asInterface(IBinder binder) { | |
| return IConditionProvider.Stub.asInterface(binder); | |
| } | |
| @Override | |
| protected boolean checkType(IInterface service) { | |
| return service instanceof IConditionProvider; | |
| } | |
| @Override | |
| public void onBootPhaseAppsCanStart() { | |
| super.onBootPhaseAppsCanStart(); | |
| for (int i = 0; i < mSystemConditionProviders.size(); i++) { | |
| mSystemConditionProviders.valueAt(i).onBootComplete(); | |
| } | |
| if (mCallback != null) { | |
| mCallback.onBootComplete(); | |
| } | |
| } | |
| @Override | |
| public void onUserSwitched(int user) { | |
| super.onUserSwitched(user); | |
| if (mCallback != null) { | |
| mCallback.onUserSwitched(); | |
| } | |
| } | |
| @Override | |
| protected void onServiceAdded(ManagedServiceInfo info) { | |
| final IConditionProvider provider = provider(info); | |
| try { | |
| provider.onConnected(); | |
| } catch (RemoteException e) { | |
| // we tried | |
| } | |
| if (mCallback != null) { | |
| mCallback.onServiceAdded(info.component); | |
| } | |
| } | |
| @Override | |
| protected void onServiceRemovedLocked(ManagedServiceInfo removed) { | |
| if (removed == null) return; | |
| for (int i = mRecords.size() - 1; i >= 0; i--) { | |
| final ConditionRecord r = mRecords.get(i); | |
| if (!r.component.equals(removed.component)) continue; | |
| mRecords.remove(i); | |
| } | |
| } | |
| @Override | |
| public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uid) { | |
| if (removingPackage) { | |
| INotificationManager inm = NotificationManager.getService(); | |
| if (pkgList != null && (pkgList.length > 0)) { | |
| for (String pkgName : pkgList) { | |
| try { | |
| inm.removeAutomaticZenRules(pkgName); | |
| inm.setNotificationPolicyAccessGranted(pkgName, false); | |
| } catch (Exception e) { | |
| Slog.e(TAG, "Failed to clean up rules for " + pkgName, e); | |
| } | |
| } | |
| } | |
| } | |
| super.onPackagesChanged(removingPackage, pkgList, uid); | |
| } | |
| @Override | |
| protected boolean isValidEntry(String packageOrComponent, int userId) { | |
| return true; | |
| } | |
| public ManagedServiceInfo checkServiceToken(IConditionProvider provider) { | |
| synchronized(mMutex) { | |
| return checkServiceTokenLocked(provider); | |
| } | |
| } | |
| private Condition[] removeDuplicateConditions(String pkg, Condition[] conditions) { | |
| if (conditions == null || conditions.length == 0) return null; | |
| final int N = conditions.length; | |
| final ArrayMap<Uri, Condition> valid = new ArrayMap<Uri, Condition>(N); | |
| for (int i = 0; i < N; i++) { | |
| final Uri id = conditions[i].id; | |
| if (valid.containsKey(id)) { | |
| Slog.w(TAG, "Ignoring condition from " + pkg + " for duplicate id: " + id); | |
| continue; | |
| } | |
| valid.put(id, conditions[i]); | |
| } | |
| if (valid.size() == 0) return null; | |
| if (valid.size() == N) return conditions; | |
| final Condition[] rt = new Condition[valid.size()]; | |
| for (int i = 0; i < rt.length; i++) { | |
| rt[i] = valid.valueAt(i); | |
| } | |
| return rt; | |
| } | |
| private ConditionRecord getRecordLocked(Uri id, ComponentName component, boolean create) { | |
| if (id == null || component == null) return null; | |
| final int N = mRecords.size(); | |
| for (int i = 0; i < N; i++) { | |
| final ConditionRecord r = mRecords.get(i); | |
| if (r.id.equals(id) && r.component.equals(component)) { | |
| return r; | |
| } | |
| } | |
| if (create) { | |
| final ConditionRecord r = new ConditionRecord(id, component); | |
| mRecords.add(r); | |
| return r; | |
| } | |
| return null; | |
| } | |
| public void notifyConditions(String pkg, ManagedServiceInfo info, Condition[] conditions) { | |
| synchronized(mMutex) { | |
| if (DEBUG) Slog.d(TAG, "notifyConditions pkg=" + pkg + " info=" + info + " conditions=" | |
| + (conditions == null ? null : Arrays.asList(conditions))); | |
| conditions = removeDuplicateConditions(pkg, conditions); | |
| if (conditions == null || conditions.length == 0) return; | |
| final int N = conditions.length; | |
| for (int i = 0; i < N; i++) { | |
| final Condition c = conditions[i]; | |
| final ConditionRecord r = getRecordLocked(c.id, info.component, true /*create*/); | |
| r.info = info; | |
| r.condition = c; | |
| } | |
| } | |
| final int N = conditions.length; | |
| for (int i = 0; i < N; i++) { | |
| final Condition c = conditions[i]; | |
| if (mCallback != null) { | |
| mCallback.onConditionChanged(c.id, c); | |
| } | |
| } | |
| } | |
| public IConditionProvider findConditionProvider(ComponentName component) { | |
| if (component == null) return null; | |
| for (ManagedServiceInfo service : getServices()) { | |
| if (component.equals(service.component)) { | |
| return provider(service); | |
| } | |
| } | |
| return null; | |
| } | |
| public Condition findCondition(ComponentName component, Uri conditionId) { | |
| if (component == null || conditionId == null) return null; | |
| synchronized (mMutex) { | |
| final ConditionRecord r = getRecordLocked(conditionId, component, false /*create*/); | |
| return r != null ? r.condition : null; | |
| } | |
| } | |
| public void ensureRecordExists(ComponentName component, Uri conditionId, | |
| IConditionProvider provider) { | |
| // constructed by convention, make sure the record exists... | |
| final ConditionRecord r = getRecordLocked(conditionId, component, true /*create*/); | |
| if (r.info == null) { | |
| // ... and is associated with the in-process service | |
| r.info = checkServiceTokenLocked(provider); | |
| } | |
| } | |
| public boolean subscribeIfNecessary(ComponentName component, Uri conditionId) { | |
| synchronized (mMutex) { | |
| final ConditionRecord r = getRecordLocked(conditionId, component, false /*create*/); | |
| if (r == null) { | |
| Slog.w(TAG, "Unable to subscribe to " + component + " " + conditionId); | |
| return false; | |
| } | |
| if (r.subscribed) return true; | |
| subscribeLocked(r); | |
| return r.subscribed; | |
| } | |
| } | |
| public void unsubscribeIfNecessary(ComponentName component, Uri conditionId) { | |
| synchronized (mMutex) { | |
| final ConditionRecord r = getRecordLocked(conditionId, component, false /*create*/); | |
| if (r == null) { | |
| Slog.w(TAG, "Unable to unsubscribe to " + component + " " + conditionId); | |
| return; | |
| } | |
| if (!r.subscribed) return; | |
| unsubscribeLocked(r);; | |
| } | |
| } | |
| private void subscribeLocked(ConditionRecord r) { | |
| if (DEBUG) Slog.d(TAG, "subscribeLocked " + r); | |
| final IConditionProvider provider = provider(r); | |
| RemoteException re = null; | |
| if (provider != null) { | |
| try { | |
| Slog.d(TAG, "Subscribing to " + r.id + " with " + r.component); | |
| provider.onSubscribe(r.id); | |
| r.subscribed = true; | |
| } catch (RemoteException e) { | |
| Slog.w(TAG, "Error subscribing to " + r, e); | |
| re = e; | |
| } | |
| } | |
| ZenLog.traceSubscribe(r != null ? r.id : null, provider, re); | |
| } | |
| @SafeVarargs | |
| private static <T> ArraySet<T> safeSet(T... items) { | |
| final ArraySet<T> rt = new ArraySet<T>(); | |
| if (items == null || items.length == 0) return rt; | |
| final int N = items.length; | |
| for (int i = 0; i < N; i++) { | |
| final T item = items[i]; | |
| if (item != null) { | |
| rt.add(item); | |
| } | |
| } | |
| return rt; | |
| } | |
| private void unsubscribeLocked(ConditionRecord r) { | |
| if (DEBUG) Slog.d(TAG, "unsubscribeLocked " + r); | |
| final IConditionProvider provider = provider(r); | |
| RemoteException re = null; | |
| if (provider != null) { | |
| try { | |
| provider.onUnsubscribe(r.id); | |
| } catch (RemoteException e) { | |
| Slog.w(TAG, "Error unsubscribing to " + r, e); | |
| re = e; | |
| } | |
| r.subscribed = false; | |
| } | |
| ZenLog.traceUnsubscribe(r != null ? r.id : null, provider, re); | |
| } | |
| private static IConditionProvider provider(ConditionRecord r) { | |
| return r == null ? null : provider(r.info); | |
| } | |
| private static IConditionProvider provider(ManagedServiceInfo info) { | |
| return info == null ? null : (IConditionProvider) info.service; | |
| } | |
| private static class ConditionRecord { | |
| public final Uri id; | |
| public final ComponentName component; | |
| public Condition condition; | |
| public ManagedServiceInfo info; | |
| public boolean subscribed; | |
| private ConditionRecord(Uri id, ComponentName component) { | |
| this.id = id; | |
| this.component = component; | |
| } | |
| @Override | |
| public String toString() { | |
| final StringBuilder sb = new StringBuilder("ConditionRecord[id=") | |
| .append(id).append(",component=").append(component) | |
| .append(",subscribed=").append(subscribed); | |
| return sb.append(']').toString(); | |
| } | |
| } | |
| public interface Callback { | |
| void onBootComplete(); | |
| void onServiceAdded(ComponentName component); | |
| void onConditionChanged(Uri id, Condition condition); | |
| void onUserSwitched(); | |
| } | |
| } |