/* * Copyright (c) 2011-2015 CyanogenMod 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 org.cyanogenmod.platform.internal; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.ServiceConnection; import android.database.ContentObserver; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiSsid; import android.os.Message; import android.util.ArraySet; import com.android.internal.policy.IKeyguardService; import cyanogenmod.providers.CMSettings; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import android.app.ActivityManagerNative; import android.app.NotificationGroup; import android.app.backup.BackupManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.XmlResourceParser; import android.os.Environment; import android.os.Handler; import android.os.UserHandle; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import android.os.ParcelUuid; import com.android.server.SystemService; import cyanogenmod.app.CMContextConstants; import cyanogenmod.app.Profile; import cyanogenmod.app.ProfileGroup; import cyanogenmod.app.ProfileManager; import cyanogenmod.app.IProfileManager; import java.util.Collection; 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.List; import java.util.Map; import java.util.Set; import java.util.UUID; /** @hide */ public class ProfileManagerService extends CMSystemService { private static final String TAG = "CMProfileService"; // Enable the below for detailed logging of this class private static final boolean LOCAL_LOGV = false; public static final String KEYGUARD_PACKAGE = "com.android.systemui"; public static final String KEYGUARD_CLASS = "com.android.systemui.keyguard.KeyguardService"; /* package */ static final File PROFILE_FILE = new File(Environment.getSystemSecureDirectory(), "profiles.xml"); private static final int MSG_SEND_PROFILE_STATE = 10; 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 Handler mHandler; private boolean mDirty; private BackupManager mBackupManager; private ProfileTriggerHelper mTriggerHelper; private Profile mEmptyProfile; private Runnable mBindKeyguard = new Runnable() { @Override public void run() { bindKeyguard(); } }; private IKeyguardService mKeyguardService; private final ServiceConnection mKeyguardConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { if (LOCAL_LOGV) Log.v(TAG, "*** Keyguard connected (yay!)"); mKeyguardService = IKeyguardService.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { if (LOCAL_LOGV) Log.v(TAG, "*** Keyguard disconnected, retrying connection soon."); mKeyguardService = null; // system UI died? retry connection in 5s mHandler.removeCallbacks(mBindKeyguard); mHandler.postDelayed(mBindKeyguard, 5000); } }; 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(); } } }; private final Handler.Callback mHandlerCallback = new Handler.Callback() { @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_SEND_PROFILE_STATE: Intent newState = new Intent(ProfileManager.PROFILES_STATE_CHANGED_ACTION); newState.putExtra(ProfileManager.EXTRA_PROFILES_STATE, msg.arg1); mContext.sendBroadcastAsUser(newState, UserHandle.ALL); if (ProfileManager.PROFILES_STATE_ENABLED == msg.arg1) { maybeApplyActiveProfile(); } return true; } return false; } }; private void maybeApplyActiveProfile() { final List wiFiTriggers = mActiveProfile.getTriggersFromType(Profile.TriggerType.WIFI); final List blueToothTriggers = mActiveProfile.getTriggersFromType(Profile.TriggerType.BLUETOOTH); boolean selectProfile = false; if (wiFiTriggers.size() == 0 && blueToothTriggers.size() == 0) { selectProfile = true; } else { final String activeSSID = getActiveSSID(); if (activeSSID != null) { for (Profile.ProfileTrigger trigger : wiFiTriggers) { if (trigger.getState() == Profile.TriggerState.ON_CONNECT && trigger.getId().equals(activeSSID)) { selectProfile = true; break; } } } if (!selectProfile && blueToothTriggers.size() > 0) { final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); final Set pairedDevices = mBluetoothAdapter.getBondedDevices(); final Set connectedBTDevices = new ArraySet<>(); for (BluetoothDevice device : pairedDevices) { if (device.isConnected()) connectedBTDevices.add(device.getAddress()); } for (Profile.ProfileTrigger trigger : blueToothTriggers) { if (connectedBTDevices.contains(trigger.getId()) && trigger.getState() == Profile.TriggerState.ON_CONNECT) { selectProfile = true; break; } } } } if (selectProfile) mActiveProfile.doSelect(mContext, mKeyguardService); } private String getActiveSSID() { final WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); final WifiInfo wifiinfo = wifiManager.getConnectionInfo(); if (wifiinfo == null) { return null; } final WifiSsid ssid = wifiinfo.getWifiSsid(); if (ssid == null) { return null; } return ssid.toString(); } private class ProfilesObserver extends ContentObserver { public ProfilesObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange, Uri uri) { int state = CMSettings.System.getInt(mContext.getContentResolver(), CMSettings.System.SYSTEM_PROFILES_ENABLED, ProfileManager.PROFILES_STATE_ENABLED); mHandler.obtainMessage(MSG_SEND_PROFILE_STATE, state, 0 /* unused */).sendToTarget(); } } public ProfileManagerService(Context context) { super(context); mContext = context; mHandler = new Handler(mHandlerCallback); if (context.getPackageManager().hasSystemFeature( CMContextConstants.Features.PROFILES)) { publishBinderService(CMContextConstants.CM_PROFILE_SERVICE, mService); } else { Log.wtf(TAG, "CM profile service started by system server but feature xml not" + " declared. Not publishing binder service!"); } } @Override public String getFeatureDeclaration() { return CMContextConstants.Features.PROFILES; } @Override public void onStart() { mBackupManager = new BackupManager(mContext); mWildcardGroup = new NotificationGroup( mContext.getString(org.cyanogenmod.platform.internal.R.string.wildcardProfile), org.cyanogenmod.platform.internal.R.string.wildcardProfile, mWildcardUUID); initialize(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); filter.addAction(Intent.ACTION_SHUTDOWN); mContext.registerReceiver(mIntentReceiver, filter); } private void bindKeyguard() { if (mKeyguardService == null) { Intent intent = new Intent(); intent.setClassName(KEYGUARD_PACKAGE, KEYGUARD_CLASS); if (!mContext.bindServiceAsUser(intent, mKeyguardConnection, Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) { Log.e(TAG, "error binding to keyguard service"); } } } @Override public void onBootPhase(int phase) { if (phase == PHASE_ACTIVITY_MANAGER_READY) { bindKeyguard(); } else if (phase == PHASE_BOOT_COMPLETED) { mContext.getContentResolver().registerContentObserver( CMSettings.System.getUriFor(CMSettings.System.SYSTEM_PROFILES_ENABLED), false, new ProfilesObserver(mHandler), UserHandle.USER_ALL); } } private void initialize() { initialize(false); } private void initialize(boolean skipFile) { mTriggerHelper = new ProfileTriggerHelper(mContext, mHandler, this); mProfiles = new HashMap(); mProfileNames = new HashMap(); mGroups = new HashMap(); mEmptyProfile = new Profile("EmptyProfile"); mDirty = false; boolean init = skipFile; if (!skipFile) { try { loadFromFile(); } 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 final IBinder mService = new IProfileManager.Stub() { @Override public void resetAll() { long token = clearCallingIdentity(); enforceChangePermissions(); initialize(true); restoreCallingIdentity(token); } @Override @Deprecated public boolean setActiveProfileByName(String profileName) { if (!isEnabled()) { Log.w(TAG, "Unable to set active profile because profiles are disabled."); return false; } if (!mProfileNames.containsKey(profileName)) { // 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; } if (LOCAL_LOGV) { Log.v(TAG, "setActiveProfile(String) found profile name in mProfileNames."); } /* * 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(); setActiveProfileInternal(mProfiles.get(mProfileNames.get(profileName)), true); restoreCallingIdentity(token); return true; } @Override public boolean setActiveProfile(ParcelUuid profileParcelUuid) { if (!isEnabled()) { Log.w(TAG, "Unable to set active profile because profiles are disabled."); return false; } /* * 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(); boolean ret = setActiveProfileInternal(profileParcelUuid.getUuid(), true); restoreCallingIdentity(token); return ret; } @Override public boolean addProfile(Profile profile) { enforceChangePermissions(); addProfileInternal(profile); long token = clearCallingIdentity(); persistIfDirty(); restoreCallingIdentity(token); return true; } @Override @Deprecated public Profile getProfileByName(String profileName) { 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 getProfileInternal(profileUuid); } @Override public Profile[] getProfiles() { Profile[] profiles = getProfileList().toArray(new Profile[mProfiles.size()]); Arrays.sort(profiles); return profiles; } @Override public Profile getActiveProfile() { if (!isEnabled()) { // Profiles are not enabled, return empty profile return mEmptyProfile; } return getActiveProfileInternal(); } @Override public boolean removeProfile(Profile profile) { enforceChangePermissions(); if (mProfileNames.remove(profile.getName()) != null && mProfiles.remove(profile.getUuid()) != null) { mDirty = true; long token = clearCallingIdentity(); persistIfDirty(); restoreCallingIdentity(token); return true; } else { return false; } } @Override public void updateProfile(Profile profile) { enforceChangePermissions(); Profile old = mProfiles.get(profile.getUuid()); if (old == null) { return; } 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 */ long token = clearCallingIdentity(); persistIfDirty(); // Also update if we changed the active profile if (mActiveProfile != null && mActiveProfile.getUuid().equals(profile.getUuid())) { setActiveProfileInternal(profile, true); } restoreCallingIdentity(token); } @Override public boolean profileExists(ParcelUuid profileUuid) { return mProfiles.containsKey(profileUuid.getUuid()); } @Override @Deprecated public boolean profileExistsByName(String profileName) { for (Map.Entry entry : mProfileNames.entrySet()) { if (entry.getKey().equalsIgnoreCase(profileName)) { return true; } } return false; } @Override @Deprecated public boolean notificationGroupExistsByName(String notificationGroupName) { for (NotificationGroup group : mGroups.values()) { if (group.getName().equalsIgnoreCase(notificationGroupName)) { return true; } } return false; } @Override public NotificationGroup[] getNotificationGroups() { return mGroups.values().toArray(new NotificationGroup[mGroups.size()]); } @Override public void addNotificationGroup(NotificationGroup group) { enforceChangePermissions(); addNotificationGroupInternal(group); long token = clearCallingIdentity(); persistIfDirty(); restoreCallingIdentity(token); } @Override public void removeNotificationGroup(NotificationGroup group) { 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()); } long token = clearCallingIdentity(); persistIfDirty(); restoreCallingIdentity(token); } @Override public void updateNotificationGroup(NotificationGroup group) { enforceChangePermissions(); NotificationGroup old = mGroups.get(group.getUuid()); if (old == null) { return; } mGroups.put(group.getUuid(), group); /* no need to set mDirty, if the group was actually changed, * it's marked as dirty by itself */ long token = clearCallingIdentity(); persistIfDirty(); restoreCallingIdentity(token); } @Override public NotificationGroup getNotificationGroupForPackage(String pkg) { for (NotificationGroup group : mGroups.values()) { if (group.hasPackage(pkg)) { return group; } } return null; } @Override public NotificationGroup getNotificationGroup(ParcelUuid uuid) { if (uuid.getUuid().equals(mWildcardGroup.getUuid())) { return mWildcardGroup; } return mGroups.get(uuid.getUuid()); } @Override public boolean isEnabled() { long token = clearCallingIdentity(); try { return CMSettings.System.getIntForUser(mContext.getContentResolver(), CMSettings.System.SYSTEM_PROFILES_ENABLED, ProfileManager.PROFILES_STATE_ENABLED, UserHandle.USER_CURRENT) == ProfileManager.PROFILES_STATE_ENABLED; } finally { restoreCallingIdentity(token); } } }; 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)); } private Profile getProfileInternal(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; } /* package */ Collection getProfileList() { return mProfiles.values(); } private String getXmlString() { StringBuilder builder = new StringBuilder(); builder.append("\n"); builder.append(TextUtils.htmlEncode(mActiveProfile.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(); } 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; mBackupManager.dataChanged(); } catch (Throwable e) { e.printStackTrace(); } } } private void enforceChangePermissions() { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MODIFY_PROFILES, "You do not have permissions to change the Profile Manager."); } // 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 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 { 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. setActiveProfileInternal(UUID.fromString(active), false); } catch (IllegalArgumentException e) { if (mProfileNames.containsKey(active)) { setActiveProfileInternal(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. setActiveProfileInternal(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 XmlPullParserException, IOException { XmlResourceParser xml = mContext.getResources().getXml( org.cyanogenmod.platform.internal.R.xml.profile_default); try { loadXml(xml, mContext); mDirty = true; persistIfDirty(); } finally { xml.close(); } } private boolean setActiveProfileInternal(UUID profileUuid, boolean doInit) { if (!mProfiles.containsKey(profileUuid)) { Log.e(TAG, "Cannot set active profile to: " + profileUuid.toString() + " - does not exist."); return false; } if (LOCAL_LOGV) Log.v(TAG, "setActiveProfile(UUID, boolean) found UUID in mProfiles."); setActiveProfileInternal(mProfiles.get(profileUuid), doInit); return true; } /* package */ Profile getActiveProfileInternal() { return mActiveProfile; } /* package */ void setActiveProfileInternal(Profile newActiveProfile, boolean doInit) { /* * 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. */ 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"); // Call profile's "doSelect" mActiveProfile.doSelect(mContext, mKeyguardService); // Notify other applications of newly selected profile. Intent broadcast = new Intent(ProfileManager.INTENT_ACTION_PROFILE_SELECTED); broadcast.putExtra(ProfileManager.EXTRA_PROFILE_NAME, mActiveProfile.getName()); broadcast.putExtra(ProfileManager.EXTRA_PROFILE_UUID, mActiveProfile.getUuid().toString()); broadcast.putExtra(ProfileManager.EXTRA_LAST_PROFILE_NAME, lastProfile.getName()); broadcast.putExtra(ProfileManager.EXTRA_LAST_PROFILE_UUID, lastProfile.getUuid().toString()); mContext.sendBroadcastAsUser(broadcast, UserHandle.ALL); persistIfDirty(); } else if (lastProfile != mActiveProfile && ActivityManagerNative.isSystemReady()) { // Something definitely changed: notify. Intent broadcast = new Intent(ProfileManager.INTENT_ACTION_PROFILE_UPDATED); broadcast.putExtra(ProfileManager.EXTRA_PROFILE_NAME, mActiveProfile.getName()); broadcast.putExtra(ProfileManager.EXTRA_PROFILE_UUID, mActiveProfile.getUuid().toString()); mContext.sendBroadcastAsUser(broadcast, UserHandle.ALL); } } 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; } }