blob: dbfb1cf5f5065bea5eb42f758e63b793ede3c6c7 [file] [log] [blame]
/**
* 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.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.notification.Condition;
import android.service.notification.ConditionProviderService;
import android.service.notification.IConditionListener;
import android.service.notification.IConditionProvider;
import android.service.notification.ZenModeConfig;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import com.android.internal.R;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
public class ConditionProviders extends ManagedServices {
private static final Condition[] NO_CONDITIONS = new Condition[0];
private final ZenModeHelper mZenModeHelper;
private final ArrayMap<IBinder, IConditionListener> mListeners
= new ArrayMap<IBinder, IConditionListener>();
private final ArrayList<ConditionRecord> mRecords = new ArrayList<ConditionRecord>();
private final CountdownConditionHelper mCountdownHelper = new CountdownConditionHelper();
public ConditionProviders(Context context, Handler handler,
UserProfiles userProfiles, ZenModeHelper zenModeHelper) {
super(context, handler, new Object(), userProfiles);
mZenModeHelper = zenModeHelper;
mZenModeHelper.addCallback(new ZenModeHelperCallback());
loadZenConfig();
}
@Override
protected Config getConfig() {
Config c = new Config();
c.caption = "condition provider";
c.serviceInterface = ConditionProviderService.SERVICE_INTERFACE;
c.secureSettingName = Settings.Secure.ENABLED_CONDITION_PROVIDERS;
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) {
super.dump(pw);
synchronized(mMutex) {
pw.print(" mListeners("); pw.print(mListeners.size()); pw.println("):");
for (int i = 0; i < mListeners.size(); i++) {
pw.print(" "); pw.println(mListeners.keyAt(i));
}
pw.print(" mRecords("); pw.print(mRecords.size()); pw.println("):");
for (int i = 0; i < mRecords.size(); i++) {
pw.print(" "); pw.println(mRecords.get(i));
}
pw.print(" mCountdownHelper: ");
pw.println(mCountdownHelper.getCurrentConditionDescription());
}
}
@Override
protected IInterface asInterface(IBinder binder) {
return IConditionProvider.Stub.asInterface(binder);
}
@Override
protected void onServiceAdded(ManagedServiceInfo info) {
Slog.d(TAG, "onServiceAdded " + info);
final IConditionProvider provider = provider(info);
try {
provider.onConnected();
} catch (RemoteException e) {
// we tried
}
synchronized (mMutex) {
final int N = mRecords.size();
for(int i = 0; i < N; i++) {
final ConditionRecord r = mRecords.get(i);
if (!r.component.equals(info.component)) continue;
r.info = info;
// if automatic, auto-subscribe
if (r.isAutomatic) {
try {
final Uri id = r.id;
if (DEBUG) Slog.d(TAG, "Auto-subscribing to configured condition " + id);
provider.onSubscribe(id);
} catch (RemoteException e) {
// we tried
}
}
}
}
}
@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;
if (r.isManual) {
// removing the current manual condition, exit zen
mZenModeHelper.setZenMode(Global.ZEN_MODE_OFF);
}
if (r.isAutomatic) {
// removing an automatic condition, exit zen
mZenModeHelper.setZenMode(Global.ZEN_MODE_OFF);
}
mRecords.remove(i);
}
}
public ManagedServiceInfo checkServiceToken(IConditionProvider provider) {
synchronized(mMutex) {
return checkServiceTokenLocked(provider);
}
}
public void requestZenModeConditions(IConditionListener callback, int relevance) {
synchronized(mMutex) {
if (DEBUG) Slog.d(TAG, "requestZenModeConditions callback=" + callback
+ " relevance=" + Condition.relevanceToString(relevance));
if (callback == null) return;
relevance = relevance & (Condition.FLAG_RELEVANT_NOW | Condition.FLAG_RELEVANT_ALWAYS);
if (relevance != 0) {
mListeners.put(callback.asBinder(), callback);
requestConditionsLocked(relevance);
} else {
mListeners.remove(callback.asBinder());
if (mListeners.isEmpty()) {
requestConditionsLocked(0);
}
}
}
}
private Condition[] validateConditions(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 (!Condition.isValidId(id, pkg)) {
Slog.w(TAG, "Ignoring condition from " + pkg + " for invalid id: " + id);
continue;
}
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) {
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;
}
}
final ConditionRecord r = new ConditionRecord(id, component);
mRecords.add(r);
return r;
}
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 = validateConditions(pkg, conditions);
if (conditions == null || conditions.length == 0) return;
final int N = conditions.length;
for (IConditionListener listener : mListeners.values()) {
try {
listener.onConditionsReceived(conditions);
} catch (RemoteException e) {
Slog.w(TAG, "Error sending conditions to listener " + listener, e);
}
}
for (int i = 0; i < N; i++) {
final Condition c = conditions[i];
final ConditionRecord r = getRecordLocked(c.id, info.component);
r.info = info;
r.condition = c;
// if manual, exit zen if false (or failed)
if (r.isManual) {
if (c.state == Condition.STATE_FALSE || c.state == Condition.STATE_ERROR) {
final boolean failed = c.state == Condition.STATE_ERROR;
if (failed) {
Slog.w(TAG, "Exit zen: manual condition failed: " + c);
} else if (DEBUG) {
Slog.d(TAG, "Exit zen: manual condition false: " + c);
}
mZenModeHelper.setZenMode(Settings.Global.ZEN_MODE_OFF);
unsubscribeLocked(r);
r.isManual = false;
}
}
// if automatic, exit zen if false (or failed), enter zen if true
if (r.isAutomatic) {
if (c.state == Condition.STATE_FALSE || c.state == Condition.STATE_ERROR) {
final boolean failed = c.state == Condition.STATE_ERROR;
if (failed) {
Slog.w(TAG, "Exit zen: automatic condition failed: " + c);
} else if (DEBUG) {
Slog.d(TAG, "Exit zen: automatic condition false: " + c);
}
mZenModeHelper.setZenMode(Settings.Global.ZEN_MODE_OFF);
} else if (c.state == Condition.STATE_TRUE) {
Slog.d(TAG, "Enter zen: automatic condition true: " + c);
mZenModeHelper.setZenMode(Settings.Global.ZEN_MODE_ON);
}
}
}
}
}
public void setZenModeCondition(Uri conditionId) {
if (DEBUG) Slog.d(TAG, "setZenModeCondition " + conditionId);
synchronized(mMutex) {
final int N = mRecords.size();
for (int i = 0; i < N; i++) {
final ConditionRecord r = mRecords.get(i);
final boolean idEqual = r.id.equals(conditionId);
if (r.isManual && !idEqual) {
// was previous manual condition, unsubscribe
unsubscribeLocked(r);
r.isManual = false;
} else if (idEqual && !r.isManual) {
// is new manual condition, subscribe
subscribeLocked(r);
r.isManual = true;
}
}
}
mCountdownHelper.setZenModeCondition(conditionId);
}
private void subscribeLocked(ConditionRecord r) {
if (DEBUG) Slog.d(TAG, "subscribeLocked " + r);
final IConditionProvider provider = provider(r);
if (provider == null) {
Slog.w(TAG, "subscribeLocked: no provider");
return;
}
try {
provider.onSubscribe(r.id);
} catch (RemoteException e) {
Slog.w(TAG, "Error subscribing to " + r, e);
}
}
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;
}
public void setAutomaticZenModeConditions(Uri[] conditionIds) {
setAutomaticZenModeConditions(conditionIds, true /*save*/);
}
private void setAutomaticZenModeConditions(Uri[] conditionIds, boolean save) {
if (DEBUG) Slog.d(TAG, "setAutomaticZenModeConditions "
+ (conditionIds == null ? null : Arrays.asList(conditionIds)));
synchronized(mMutex) {
final ArraySet<Uri> newIds = safeSet(conditionIds);
final int N = mRecords.size();
boolean changed = false;
for (int i = 0; i < N; i++) {
final ConditionRecord r = mRecords.get(i);
final boolean automatic = newIds.contains(r.id);
if (!r.isAutomatic && automatic) {
// subscribe to new automatic
subscribeLocked(r);
r.isAutomatic = true;
changed = true;
} else if (r.isAutomatic && !automatic) {
// unsubscribe from old automatic
unsubscribeLocked(r);
r.isAutomatic = false;
changed = true;
}
}
if (save && changed) {
saveZenConfigLocked();
}
}
}
public Condition[] getAutomaticZenModeConditions() {
synchronized(mMutex) {
final int N = mRecords.size();
ArrayList<Condition> rt = null;
for (int i = 0; i < N; i++) {
final ConditionRecord r = mRecords.get(i);
if (r.isAutomatic && r.condition != null) {
if (rt == null) rt = new ArrayList<Condition>();
rt.add(r.condition);
}
}
return rt == null ? NO_CONDITIONS : rt.toArray(new Condition[rt.size()]);
}
}
private void unsubscribeLocked(ConditionRecord r) {
if (DEBUG) Slog.d(TAG, "unsubscribeLocked " + r);
final IConditionProvider provider = provider(r);
if (provider == null) {
Slog.w(TAG, "unsubscribeLocked: no provider");
return;
}
try {
provider.onUnsubscribe(r.id);
} catch (RemoteException e) {
Slog.w(TAG, "Error unsubscribing to " + r, e);
}
}
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 void requestConditionsLocked(int flags) {
for (ManagedServiceInfo info : mServices) {
final IConditionProvider provider = provider(info);
if (provider == null) continue;
try {
provider.onRequestConditions(flags);
} catch (RemoteException e) {
Slog.w(TAG, "Error requesting conditions from " + info.component, e);
}
}
}
private void loadZenConfig() {
final ZenModeConfig config = mZenModeHelper.getConfig();
if (config == null) {
if (DEBUG) Slog.d(TAG, "loadZenConfig: no config");
return;
}
synchronized (mMutex) {
if (config.conditionComponents == null || config.conditionIds == null
|| config.conditionComponents.length != config.conditionIds.length) {
if (DEBUG) Slog.d(TAG, "loadZenConfig: no conditions");
setAutomaticZenModeConditions(null, false /*save*/);
return;
}
final ArraySet<Uri> newIds = new ArraySet<Uri>();
final int N = config.conditionComponents.length;
for (int i = 0; i < N; i++) {
final ComponentName component = config.conditionComponents[i];
final Uri id = config.conditionIds[i];
if (component != null && id != null) {
getRecordLocked(id, component); // ensure record exists
newIds.add(id);
}
}
if (DEBUG) Slog.d(TAG, "loadZenConfig: N=" + N);
setAutomaticZenModeConditions(newIds.toArray(new Uri[newIds.size()]), false /*save*/);
}
}
private void saveZenConfigLocked() {
ZenModeConfig config = mZenModeHelper.getConfig();
if (config == null) return;
config = config.copy();
final ArrayList<ConditionRecord> automatic = new ArrayList<ConditionRecord>();
final int automaticN = mRecords.size();
for (int i = 0; i < automaticN; i++) {
final ConditionRecord r = mRecords.get(i);
if (r.isAutomatic) {
automatic.add(r);
}
}
if (automatic.isEmpty()) {
config.conditionComponents = null;
config.conditionIds = null;
} else {
final int N = automatic.size();
config.conditionComponents = new ComponentName[N];
config.conditionIds = new Uri[N];
for (int i = 0; i < N; i++) {
final ConditionRecord r = automatic.get(i);
config.conditionComponents[i] = r.component;
config.conditionIds[i] = r.id;
}
}
if (DEBUG) Slog.d(TAG, "Setting zen config to: " + config);
mZenModeHelper.setConfig(config);
}
private final class CountdownConditionHelper extends BroadcastReceiver {
private static final String ACTION = "CountdownConditionHelper";
private static final int REQUEST_CODE = 100;
private static final String EXTRA_TIME = "time";
private long mCurrent;
public CountdownConditionHelper() {
mContext.registerReceiver(this, new IntentFilter(ACTION));
}
public void setZenModeCondition(Uri conditionId) {
final long time = tryParseCondition(conditionId);
final AlarmManager alarms = (AlarmManager)
mContext.getSystemService(Context.ALARM_SERVICE);
final Intent intent = new Intent(ACTION).putExtra(EXTRA_TIME, time)
.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarms.cancel(pendingIntent);
mCurrent = time;
if (time > 0) {
final long now = System.currentTimeMillis();
final CharSequence span =
DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future (%s), now=%s",
ACTION, ts(time), time - now, span, ts(now)));
alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}
}
public String getCurrentConditionDescription() {
if (mCurrent == 0) return null;
final long time = mCurrent;
final long now = System.currentTimeMillis();
final CharSequence span =
DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return String.format("Scheduled for %s, %s in the future (%s), now=%s",
ts(time), time - now, span, ts(now));
}
private String ts(long time) {
return new Date(time) + " (" + time + ")";
}
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION.equals(intent.getAction())) {
final long time = intent.getLongExtra(EXTRA_TIME, 0);
Slog.d(TAG, "Countdown condition fired. time=" + time + " mCurrent=" + mCurrent);
if (time > 0 && time == mCurrent) {
// countdown condition is still the manual condition, leave zen
mZenModeHelper.setZenMode(Global.ZEN_MODE_OFF);
ConditionProviders.this.setZenModeCondition(null);
}
}
}
private long tryParseCondition(Uri conditionId) {
// condition://android/countdown/1399917958951
if (!Condition.isValidId(conditionId, "android")) return 0;
if (conditionId.getPathSegments().size() != 2
|| !"countdown".equals(conditionId.getPathSegments().get(0))) return 0;
try {
return Long.parseLong(conditionId.getPathSegments().get(1));
} catch (RuntimeException e) {
Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
return 0;
}
}
}
private class ZenModeHelperCallback extends ZenModeHelper.Callback {
@Override
void onConfigChanged() {
loadZenConfig();
}
@Override
void onZenModeChanged() {
final int mode = mZenModeHelper.getZenMode();
if (mode == Global.ZEN_MODE_OFF) {
// ensure any manual condition is cleared
setZenModeCondition(null);
}
}
}
private static class ConditionRecord {
public final Uri id;
public final ComponentName component;
public Condition condition;
public ManagedServiceInfo info;
public boolean isAutomatic;
public boolean isManual;
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);
if (isAutomatic) sb.append(",automatic");
if (isManual) sb.append(",manual");
return sb.append(']').toString();
}
}
}