/*
* Copyright (c) 2011, 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;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import android.app.ActivityManagerNative;
import android.app.IProfileManager;
import android.app.NotificationGroup;
import android.app.Profile;
import android.app.ProfileGroup;
import android.app.backup.BackupManager;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.XmlResourceParser;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
import android.net.wifi.WifiInfo;
import android.os.Environment;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.os.ParcelUuid;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/** {@hide} */
public class ProfileManagerService extends IProfileManager.Stub {
// Enable the below for detailed logging of this class
private static final boolean LOCAL_LOGV = false;
/**
*
Broadcast Action: A new profile has been selected. This can be triggered by the user
* or by calls to the ProfileManagerService / Profile.
* @hide
*/
public static final String INTENT_ACTION_PROFILE_SELECTED = "android.intent.action.PROFILE_SELECTED";
/**
* Broadcast Action: Current profile has been updated. This is triggered every time the
* currently active profile is updated, instead of selected.
* For instance, this includes profile updates caused by a locale change, which doesn't
* trigger a profile selection, but causes its name to change.
* @hide
*/
public static final String INTENT_ACTION_PROFILE_UPDATED = "android.intent.action.PROFILE_UPDATED";
public static final String PERMISSION_CHANGE_SETTINGS = "android.permission.WRITE_SETTINGS";
/* package */ static final File PROFILE_FILE =
new File(Environment.getSystemSecureDirectory(), "profiles.xml");
private static final String TAG = "ProfileService";
private Map mProfiles;
// Match UUIDs and names, used for reverse compatibility
private Map mProfileNames;
private Map mGroups;
private Profile mActiveProfile;
// Well-known UUID of the wildcard group
private static final UUID mWildcardUUID = UUID.fromString("a126d48a-aaef-47c4-baed-7f0e44aeffe5");
private NotificationGroup mWildcardGroup;
private Context mContext;
private boolean mDirty;
private BackupManager mBackupManager;
private WifiManager mWifiManager;
private String mLastConnectedSSID;
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_LOCALE_CHANGED)) {
persistIfDirty();
initialize();
} else if (action.equals(Intent.ACTION_SHUTDOWN)) {
persistIfDirty();
} else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
String activeSSID = getActiveSSID();
int triggerState;
if (activeSSID != null) {
triggerState = Profile.TriggerState.ON_CONNECT;
mLastConnectedSSID = activeSSID;
} else {
triggerState = Profile.TriggerState.ON_DISCONNECT;
}
checkTriggers(Profile.TriggerType.WIFI, mLastConnectedSSID, triggerState);
} else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)
|| action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
int triggerState = action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)
? Profile.TriggerState.ON_CONNECT : Profile.TriggerState.ON_DISCONNECT;
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
checkTriggers(Profile.TriggerType.BLUETOOTH, device.getAddress(), triggerState);
}
}
private void checkTriggers(int type, String id, int newState) {
for (Profile p : mProfiles.values()) {
if (newState != p.getTrigger(type, id)) {
continue;
}
try {
if (!mActiveProfile.getUuid().equals(p.getUuid())) {
setActiveProfile(p, true);
}
} catch (RemoteException e) {
Log.e(TAG, "Could not update profile on trigger", e);
}
}
}
};
public ProfileManagerService(Context context) {
mContext = context;
mBackupManager = new BackupManager(mContext);
mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
mLastConnectedSSID = getActiveSSID();
mWildcardGroup = new NotificationGroup(
context.getString(com.android.internal.R.string.wildcardProfile),
com.android.internal.R.string.wildcardProfile,
mWildcardUUID);
initialize();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_LOCALE_CHANGED);
filter.addAction(Intent.ACTION_SHUTDOWN);
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
mContext.registerReceiver(mIntentReceiver, filter);
}
private void initialize() {
initialize(false);
}
private void initialize(boolean skipFile) {
mProfiles = new HashMap();
mProfileNames = new HashMap();
mGroups = new HashMap();
mDirty = false;
boolean init = skipFile;
if (!skipFile) {
try {
loadFromFile();
} catch (RemoteException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
init = true;
} catch (IOException e) {
init = true;
}
}
if (init) {
try {
initialiseStructure();
} catch (Throwable ex) {
Log.e(TAG, "Error loading xml from resource: ", ex);
}
}
}
private String getActiveSSID() {
WifiInfo wifiinfo = mWifiManager.getConnectionInfo();
if (wifiinfo == null) {
return null;
}
WifiSsid ssid = wifiinfo.getWifiSsid();
if (ssid == null) {
return null;
}
return ssid.toString();
}
@Override
public void resetAll() {
enforceChangePermissions();
initialize(true);
}
@Override
@Deprecated
public boolean setActiveProfileByName(String profileName) throws RemoteException, SecurityException {
if (mProfileNames.containsKey(profileName)) {
if (LOCAL_LOGV) Log.v(TAG, "setActiveProfile(String) found profile name in mProfileNames.");
return setActiveProfile(mProfiles.get(mProfileNames.get(profileName)), true);
} else {
// Since profileName could not be casted into a UUID, we can call it a string.
Log.w(TAG, "Unable to find profile to set active, based on string: " + profileName);
return false;
}
}
@Override
public boolean setActiveProfile(ParcelUuid profileParcelUuid) throws RemoteException, SecurityException {
UUID profileUuid = profileParcelUuid.getUuid();
if(mProfiles.containsKey(profileUuid)){
if (LOCAL_LOGV) Log.v(TAG, "setActiveProfileByUuid(ParcelUuid) found profile UUID in mProfileNames.");
return setActiveProfile(mProfiles.get(profileUuid), true);
} else {
Log.e(TAG, "Cannot set active profile to: " + profileUuid.toString() + " - does not exist.");
return false;
}
}
private boolean setActiveProfile(UUID profileUuid, boolean doinit) throws RemoteException {
if(mProfiles.containsKey(profileUuid)){
if (LOCAL_LOGV) Log.v(TAG, "setActiveProfile(UUID, boolean) found profile UUID in mProfiles.");
return setActiveProfile(mProfiles.get(profileUuid), doinit);
} else {
Log.e(TAG, "Cannot set active profile to: " + profileUuid.toString() + " - does not exist.");
return false;
}
}
private boolean setActiveProfile(Profile newActiveProfile, boolean doinit) throws RemoteException {
/*
* NOTE: Since this is not a public function, and all public functions
* take either a string or a UUID, the active profile should always be
* in the collection. If writing another setActiveProfile which receives
* a Profile object, run enforceChangePermissions, add the profile to the
* list, and THEN add it.
*/
try {
enforceChangePermissions();
Log.d(TAG, "Set active profile to: " + newActiveProfile.getUuid().toString() + " - " + newActiveProfile.getName());
Profile lastProfile = mActiveProfile;
mActiveProfile = newActiveProfile;
mDirty = true;
if (doinit) {
if (LOCAL_LOGV) Log.v(TAG, "setActiveProfile(Profile, boolean) - Running init");
/*
* We need to clear the caller's identity in order to
* - allow the profile switch to execute actions not included in the caller's permissions
* - broadcast INTENT_ACTION_PROFILE_SELECTED
*/
long token = clearCallingIdentity();
// Call profile's "doSelect"
mActiveProfile.doSelect(mContext);
// Notify other applications of newly selected profile.
Intent broadcast = new Intent(INTENT_ACTION_PROFILE_SELECTED);
broadcast.putExtra("name", mActiveProfile.getName());
broadcast.putExtra("uuid", mActiveProfile.getUuid().toString());
broadcast.putExtra("lastName", lastProfile.getName());
broadcast.putExtra("lastUuid", lastProfile.getUuid().toString());
mContext.sendBroadcastAsUser(broadcast, UserHandle.ALL);
restoreCallingIdentity(token);
persistIfDirty();
} else if (lastProfile != mActiveProfile &&
ActivityManagerNative.isSystemReady()) {
// Something definitely changed: notify.
long token = clearCallingIdentity();
Intent broadcast = new Intent(INTENT_ACTION_PROFILE_UPDATED);
broadcast.putExtra("name", mActiveProfile.getName());
broadcast.putExtra("uuid", mActiveProfile.getUuid().toString());
mContext.sendBroadcastAsUser(broadcast, UserHandle.ALL);
restoreCallingIdentity(token);
}
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
@Override
public boolean addProfile(Profile profile) throws RemoteException, SecurityException {
enforceChangePermissions();
addProfileInternal(profile);
persistIfDirty();
return true;
}
private void addProfileInternal(Profile profile) {
// Make sure this profile has all of the correct groups.
for (NotificationGroup group : mGroups.values()) {
ensureGroupInProfile(profile, group, false);
}
ensureGroupInProfile(profile, mWildcardGroup, true);
mProfiles.put(profile.getUuid(), profile);
mProfileNames.put(profile.getName(), profile.getUuid());
mDirty = true;
}
private void ensureGroupInProfile(Profile profile, NotificationGroup group, boolean defaultGroup) {
if (profile.getProfileGroup(group.getUuid()) != null) {
return;
}
/* enforce a matchup between profile and notification group, which not only
* works by UUID, but also by name for backwards compatibility */
for (ProfileGroup pg : profile.getProfileGroups()) {
if (pg.matches(group, defaultGroup)) {
return;
}
}
/* didn't find any, create new group */
profile.addProfileGroup(new ProfileGroup(group.getUuid(), defaultGroup));
}
@Override
@Deprecated
public Profile getProfileByName(String profileName) throws RemoteException {
if (mProfileNames.containsKey(profileName)) {
return mProfiles.get(mProfileNames.get(profileName));
} else if (mProfiles.containsKey(UUID.fromString((profileName)))) {
return mProfiles.get(UUID.fromString(profileName));
} else {
return null;
}
}
@Override
public Profile getProfile(ParcelUuid profileParcelUuid) {
UUID profileUuid = profileParcelUuid.getUuid();
return getProfile(profileUuid);
}
public Profile getProfile(UUID profileUuid) {
// use primary UUID first
if (mProfiles.containsKey(profileUuid)) {
return mProfiles.get(profileUuid);
}
// if no match was found: try secondary UUID
for (Profile p : mProfiles.values()) {
for (UUID uuid : p.getSecondaryUuids()) {
if (profileUuid.equals(uuid)) {
return p;
}
}
}
// nothing found
return null;
}
@Override
public Profile[] getProfiles() throws RemoteException {
Profile[] tmpArr = mProfiles.values().toArray(new Profile[mProfiles.size()]);
Arrays.sort(tmpArr);
return tmpArr;
}
@Override
public Profile getActiveProfile() throws RemoteException {
return mActiveProfile;
}
@Override
public boolean removeProfile(Profile profile) throws RemoteException, SecurityException {
enforceChangePermissions();
if (mProfileNames.remove(profile.getName()) != null && mProfiles.remove(profile.getUuid()) != null) {
mDirty = true;
persistIfDirty();
return true;
} else{
return false;
}
}
@Override
public void updateProfile(Profile profile) throws RemoteException, SecurityException {
enforceChangePermissions();
Profile old = mProfiles.get(profile.getUuid());
if (old != null) {
mProfileNames.remove(old.getName());
mProfileNames.put(profile.getName(), profile.getUuid());
mProfiles.put(profile.getUuid(), profile);
/* no need to set mDirty, if the profile was actually changed,
* it's marked as dirty by itself */
persistIfDirty();
// Also update we changed the active profile
if (mActiveProfile != null && mActiveProfile.getUuid().equals(profile.getUuid())) {
setActiveProfile(profile, true);
}
}
}
@Override
public boolean profileExists(ParcelUuid profileUuid) throws RemoteException {
return mProfiles.containsKey(profileUuid.getUuid());
}
@Override
public boolean profileExistsByName(String profileName) throws RemoteException {
for (Map.Entry entry : mProfileNames.entrySet()) {
if (entry.getKey().equalsIgnoreCase(profileName)) {
return true;
}
}
return false;
}
@Override
public boolean notificationGroupExistsByName(String notificationGroupName) throws RemoteException {
for (NotificationGroup group : mGroups.values()) {
if (group.getName().equalsIgnoreCase(notificationGroupName)) {
return true;
}
}
return false;
}
@Override
public NotificationGroup[] getNotificationGroups() throws RemoteException {
return mGroups.values().toArray(new NotificationGroup[mGroups.size()]);
}
@Override
public void addNotificationGroup(NotificationGroup group) throws RemoteException, SecurityException {
enforceChangePermissions();
addNotificationGroupInternal(group);
persistIfDirty();
}
private void addNotificationGroupInternal(NotificationGroup group) {
if (mGroups.put(group.getUuid(), group) == null) {
// If the above is true, then the ProfileGroup shouldn't exist in
// the profile. Ensure it is added.
for (Profile profile : mProfiles.values()) {
ensureGroupInProfile(profile, group, false);
}
}
mDirty = true;
}
@Override
public void removeNotificationGroup(NotificationGroup group) throws RemoteException, SecurityException {
enforceChangePermissions();
mDirty |= (mGroups.remove(group.getUuid()) != null);
// Remove the corresponding ProfileGroup from all the profiles too if
// they use it.
for (Profile profile : mProfiles.values()) {
profile.removeProfileGroup(group.getUuid());
}
persistIfDirty();
}
@Override
public void updateNotificationGroup(NotificationGroup group) throws RemoteException, SecurityException {
enforceChangePermissions();
NotificationGroup old = mGroups.get(group.getUuid());
if (old != null) {
mGroups.put(group.getUuid(), group);
/* no need to set mDirty, if the group was actually changed,
* it's marked as dirty by itself */
persistIfDirty();
}
}
@Override
public NotificationGroup getNotificationGroupForPackage(String pkg) throws RemoteException {
for (NotificationGroup group : mGroups.values()) {
if (group.hasPackage(pkg)) {
return group;
}
}
return null;
}
// Called by SystemBackupAgent after files are restored to disk.
void settingsRestored() {
initialize();
for (Profile p : mProfiles.values()) {
p.validateRingtones(mContext);
}
persistIfDirty();
}
private void loadFromFile() throws RemoteException, XmlPullParserException, IOException {
XmlPullParserFactory xppf = XmlPullParserFactory.newInstance();
XmlPullParser xpp = xppf.newPullParser();
FileReader fr = new FileReader(PROFILE_FILE);
xpp.setInput(fr);
loadXml(xpp, mContext);
fr.close();
persistIfDirty();
}
private void loadXml(XmlPullParser xpp, Context context) throws
XmlPullParserException, IOException, RemoteException {
int event = xpp.next();
String active = null;
while (event != XmlPullParser.END_TAG || !"profiles".equals(xpp.getName())) {
if (event == XmlPullParser.START_TAG) {
String name = xpp.getName();
if (name.equals("active")) {
active = xpp.nextText();
Log.d(TAG, "Found active: " + active);
} else if (name.equals("profile")) {
Profile prof = Profile.fromXml(xpp, context);
addProfileInternal(prof);
// Failsafe if no active found
if (active == null) {
active = prof.getUuid().toString();
}
} else if (name.equals("notificationGroup")) {
NotificationGroup ng = NotificationGroup.fromXml(xpp, context);
addNotificationGroupInternal(ng);
}
} else if (event == XmlPullParser.END_DOCUMENT) {
throw new IOException("Premature end of file while reading " + PROFILE_FILE);
}
event = xpp.next();
}
// Don't do initialisation on startup. The AudioManager doesn't exist yet
// and besides, the volume settings will have survived the reboot.
try {
// Try / catch block to detect if XML file needs to be upgraded.
setActiveProfile(UUID.fromString(active), false);
} catch (IllegalArgumentException e) {
if (mProfileNames.containsKey(active)) {
setActiveProfile(mProfileNames.get(active), false);
} else {
// Final fail-safe: We must have SOME profile active.
// If we couldn't select one by now, we'll pick the first in the set.
setActiveProfile(mProfiles.values().iterator().next(), false);
}
// This is a hint that we probably just upgraded the XML file. Save changes.
mDirty = true;
}
}
private void initialiseStructure() throws RemoteException, XmlPullParserException, IOException {
XmlResourceParser xml = mContext.getResources().getXml(
com.android.internal.R.xml.profile_default);
try {
loadXml(xml, mContext);
mDirty = true;
persistIfDirty();
} finally {
xml.close();
}
}
private String getXmlString() throws RemoteException {
StringBuilder builder = new StringBuilder();
builder.append("\n");
builder.append(TextUtils.htmlEncode(getActiveProfile().getUuid().toString()));
builder.append("\n");
for (Profile p : mProfiles.values()) {
p.getXmlString(builder, mContext);
}
for (NotificationGroup g : mGroups.values()) {
g.getXmlString(builder, mContext);
}
builder.append("\n");
return builder.toString();
}
@Override
public NotificationGroup getNotificationGroup(ParcelUuid uuid) throws RemoteException {
if (uuid.getUuid().equals(mWildcardGroup.getUuid())) {
return mWildcardGroup;
}
return mGroups.get(uuid.getUuid());
}
private synchronized void persistIfDirty() {
boolean dirty = mDirty;
if (!dirty) {
for (Profile profile : mProfiles.values()) {
if (profile.isDirty()) {
dirty = true;
break;
}
}
}
if (!dirty) {
for (NotificationGroup group : mGroups.values()) {
if (group.isDirty()) {
dirty = true;
break;
}
}
}
if (dirty) {
try {
Log.d(TAG, "Saving profile data...");
FileWriter fw = new FileWriter(PROFILE_FILE);
fw.write(getXmlString());
fw.close();
Log.d(TAG, "Save completed.");
mDirty = false;
long token = clearCallingIdentity();
mBackupManager.dataChanged();
restoreCallingIdentity(token);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
private void enforceChangePermissions() throws SecurityException {
mContext.enforceCallingOrSelfPermission(PERMISSION_CHANGE_SETTINGS,
"You do not have permissions to change the Profile Manager.");
}
}