fix: 首次提交
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.settingslib.location;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.UserHandle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.Immutable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Specifies a setting that is being injected into Settings > Location > Location services.
|
||||
*
|
||||
* @see android.location.SettingInjectorService
|
||||
*/
|
||||
@Immutable
|
||||
public class InjectedSetting {
|
||||
|
||||
/**
|
||||
* Package for the subclass of {@link android.location.SettingInjectorService} and for the
|
||||
* settings activity.
|
||||
*/
|
||||
public final String packageName;
|
||||
|
||||
/**
|
||||
* Class name for the subclass of {@link android.location.SettingInjectorService} that
|
||||
* specifies dynamic values for the location setting.
|
||||
*/
|
||||
public final String className;
|
||||
|
||||
/**
|
||||
* The {@link androidx.preference.Preference#getTitle()} value.
|
||||
*/
|
||||
public final String title;
|
||||
|
||||
/**
|
||||
* The {@link androidx.preference.Preference#getIcon()} value.
|
||||
*/
|
||||
public final int iconId;
|
||||
|
||||
/**
|
||||
* The user/profile associated with this setting (e.g. managed profile)
|
||||
*/
|
||||
public final UserHandle mUserHandle;
|
||||
|
||||
/**
|
||||
* The activity to launch to allow the user to modify the settings value. Assumed to be in the
|
||||
* {@link #packageName} package.
|
||||
*/
|
||||
public final String settingsActivity;
|
||||
|
||||
/**
|
||||
* The user restriction associated with this setting.
|
||||
*/
|
||||
public final String userRestriction;
|
||||
|
||||
private InjectedSetting(Builder builder) {
|
||||
this.packageName = builder.mPackageName;
|
||||
this.className = builder.mClassName;
|
||||
this.title = builder.mTitle;
|
||||
this.iconId = builder.mIconId;
|
||||
this.mUserHandle = builder.mUserHandle;
|
||||
this.settingsActivity = builder.mSettingsActivity;
|
||||
this.userRestriction = builder.mUserRestriction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InjectedSetting{" +
|
||||
"mPackageName='" + packageName + '\'' +
|
||||
", mClassName='" + className + '\'' +
|
||||
", label=" + title +
|
||||
", iconId=" + iconId +
|
||||
", userId=" + mUserHandle.getIdentifier() +
|
||||
", settingsActivity='" + settingsActivity + '\'' +
|
||||
", userRestriction='" + userRestriction +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intent to start the {@link #className} service.
|
||||
*/
|
||||
public Intent getServiceIntent() {
|
||||
Intent intent = new Intent();
|
||||
intent.setClassName(packageName, className);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof InjectedSetting)) return false;
|
||||
|
||||
InjectedSetting that = (InjectedSetting) o;
|
||||
|
||||
return Objects.equals(packageName, that.packageName)
|
||||
&& Objects.equals(className, that.className)
|
||||
&& Objects.equals(title, that.title)
|
||||
&& Objects.equals(iconId, that.iconId)
|
||||
&& Objects.equals(mUserHandle, that.mUserHandle)
|
||||
&& Objects.equals(settingsActivity, that.settingsActivity)
|
||||
&& Objects.equals(userRestriction, that.userRestriction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = packageName.hashCode();
|
||||
result = 31 * result + className.hashCode();
|
||||
result = 31 * result + title.hashCode();
|
||||
result = 31 * result + iconId;
|
||||
result = 31 * result + (mUserHandle == null ? 0 : mUserHandle.hashCode());
|
||||
result = 31 * result + settingsActivity.hashCode();
|
||||
result = 31 * result + (userRestriction == null ? 0 : userRestriction.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String mPackageName;
|
||||
private String mClassName;
|
||||
private String mTitle;
|
||||
private int mIconId;
|
||||
private UserHandle mUserHandle;
|
||||
private String mSettingsActivity;
|
||||
private String mUserRestriction;
|
||||
|
||||
public Builder setPackageName(String packageName) {
|
||||
mPackageName = packageName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setClassName(String className) {
|
||||
mClassName = className;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTitle(String title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIconId(int iconId) {
|
||||
mIconId = iconId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUserHandle(UserHandle userHandle) {
|
||||
mUserHandle = userHandle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSettingsActivity(String settingsActivity) {
|
||||
mSettingsActivity = settingsActivity;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUserRestriction(String userRestriction) {
|
||||
mUserRestriction = userRestriction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public InjectedSetting build() {
|
||||
if (mPackageName == null || mClassName == null || TextUtils.isEmpty(mTitle)
|
||||
|| TextUtils.isEmpty(mSettingsActivity)) {
|
||||
if (Log.isLoggable(SettingsInjector.TAG, Log.WARN)) {
|
||||
Log.w(SettingsInjector.TAG, "Illegal setting specification: package="
|
||||
+ mPackageName + ", class=" + mClassName
|
||||
+ ", title=" + mTitle + ", settingsActivity=" + mSettingsActivity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return new InjectedSetting(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.settingslib.location;
|
||||
|
||||
import android.app.AppOpsManager;
|
||||
import android.content.Context;
|
||||
import android.content.PermissionChecker;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.IconDrawableFactory;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Retrieves the information of applications which accessed location recently.
|
||||
*/
|
||||
public class RecentLocationApps {
|
||||
private static final String TAG = RecentLocationApps.class.getSimpleName();
|
||||
@VisibleForTesting
|
||||
static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
|
||||
|
||||
// Keep last 24 hours of location app information.
|
||||
private static final long RECENT_TIME_INTERVAL_MILLIS = DateUtils.DAY_IN_MILLIS;
|
||||
|
||||
@VisibleForTesting
|
||||
static final int[] LOCATION_REQUEST_OPS = new int[]{
|
||||
AppOpsManager.OP_MONITOR_LOCATION,
|
||||
AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
|
||||
};
|
||||
@VisibleForTesting
|
||||
static final int[] LOCATION_PERMISSION_OPS = new int[]{
|
||||
AppOpsManager.OP_FINE_LOCATION,
|
||||
AppOpsManager.OP_COARSE_LOCATION,
|
||||
};
|
||||
|
||||
private final PackageManager mPackageManager;
|
||||
private final Context mContext;
|
||||
private final IconDrawableFactory mDrawableFactory;
|
||||
|
||||
public RecentLocationApps(Context context) {
|
||||
mContext = context;
|
||||
mPackageManager = context.getPackageManager();
|
||||
mDrawableFactory = IconDrawableFactory.newInstance(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a list of applications which queried location recently within specified time.
|
||||
* Apps are sorted by recency. Apps with more recent location requests are in the front.
|
||||
*/
|
||||
public List<Request> getAppList(boolean showSystemApps) {
|
||||
// Retrieve a location usage list from AppOps
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
// Retrieve a location usage list from AppOps
|
||||
AppOpsManager aoManager =
|
||||
(AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
|
||||
List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(LOCATION_REQUEST_OPS);
|
||||
|
||||
final int appOpsCount = appOps != null ? appOps.size() : 0;
|
||||
|
||||
// Process the AppOps list and generate a preference list.
|
||||
ArrayList<Request> requests = new ArrayList<>(appOpsCount);
|
||||
final long now = System.currentTimeMillis();
|
||||
final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
|
||||
final List<UserHandle> profiles = um.getUserProfiles();
|
||||
|
||||
for (int i = 0; i < appOpsCount; ++i) {
|
||||
AppOpsManager.PackageOps ops = appOps.get(i);
|
||||
// Don't show the Android System in the list - it's not actionable for the user.
|
||||
// Also don't show apps belonging to background users except managed users.
|
||||
String packageName = ops.getPackageName();
|
||||
int uid = ops.getUid();
|
||||
final UserHandle user = UserHandle.getUserHandleForUid(uid);
|
||||
|
||||
boolean isAndroidOs =
|
||||
(uid == android.os.Process.SYSTEM_UID) && ANDROID_SYSTEM_PACKAGE_NAME.equals(
|
||||
packageName);
|
||||
if (isAndroidOs || !profiles.contains(user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't show apps that do not have user sensitive location permissions
|
||||
boolean showApp = true;
|
||||
if (!showSystemApps) {
|
||||
for (int op : LOCATION_PERMISSION_OPS) {
|
||||
final String permission = AppOpsManager.opToPermission(op);
|
||||
final int permissionFlags = pm.getPermissionFlags(permission, packageName,
|
||||
user);
|
||||
if (PermissionChecker.checkPermissionForPreflight(mContext, permission,
|
||||
PermissionChecker.PID_UNKNOWN, uid, packageName)
|
||||
== PermissionChecker.PERMISSION_GRANTED) {
|
||||
if ((permissionFlags
|
||||
& PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)
|
||||
== 0) {
|
||||
showApp = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if ((permissionFlags
|
||||
& PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) == 0) {
|
||||
showApp = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showApp) {
|
||||
Request request = getRequestFromOps(now, ops);
|
||||
if (request != null) {
|
||||
requests.add(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of apps that requested for location recently, sorting by recency.
|
||||
*
|
||||
* @param showSystemApps whether includes system apps in the list.
|
||||
* @return the list of apps that recently requested for location.
|
||||
*/
|
||||
public List<Request> getAppListSorted(boolean showSystemApps) {
|
||||
List<Request> requests = getAppList(showSystemApps);
|
||||
// Sort the list of Requests by recency. Most recent request first.
|
||||
Collections.sort(requests, Collections.reverseOrder(new Comparator<Request>() {
|
||||
@Override
|
||||
public int compare(Request request1, Request request2) {
|
||||
return Long.compare(request1.requestFinishTime, request2.requestFinishTime);
|
||||
}
|
||||
}));
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Request entry for the given PackageOps.
|
||||
*
|
||||
* This method examines the time interval of the PackageOps first. If the PackageOps is older
|
||||
* than the designated interval, this method ignores the PackageOps object and returns null.
|
||||
* When the PackageOps is fresh enough, this method returns a Request object for the package
|
||||
*/
|
||||
private Request getRequestFromOps(long now,
|
||||
AppOpsManager.PackageOps ops) {
|
||||
String packageName = ops.getPackageName();
|
||||
List<AppOpsManager.OpEntry> entries = ops.getOps();
|
||||
boolean highBattery = false;
|
||||
boolean normalBattery = false;
|
||||
long locationRequestFinishTime = 0L;
|
||||
// Earliest time for a location request to end and still be shown in list.
|
||||
long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
|
||||
for (AppOpsManager.OpEntry entry : entries) {
|
||||
if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) {
|
||||
locationRequestFinishTime = entry.getTime() + entry.getDuration();
|
||||
switch (entry.getOp()) {
|
||||
case AppOpsManager.OP_MONITOR_LOCATION:
|
||||
normalBattery = true;
|
||||
break;
|
||||
case AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION:
|
||||
highBattery = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!highBattery && !normalBattery) {
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, packageName + " hadn't used location within the time interval.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// The package is fresh enough, continue.
|
||||
int uid = ops.getUid();
|
||||
int userId = UserHandle.getUserId(uid);
|
||||
|
||||
Request request = null;
|
||||
try {
|
||||
ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
|
||||
packageName, PackageManager.GET_META_DATA, userId);
|
||||
if (appInfo == null) {
|
||||
Log.w(TAG, "Null application info retrieved for package " + packageName
|
||||
+ ", userId " + userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
final UserHandle userHandle = new UserHandle(userId);
|
||||
Drawable icon = mDrawableFactory.getBadgedIcon(appInfo, userId);
|
||||
CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo);
|
||||
CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle);
|
||||
if (appLabel.toString().contentEquals(badgedAppLabel)) {
|
||||
// If badged label is not different from original then no need for it as
|
||||
// a separate content description.
|
||||
badgedAppLabel = null;
|
||||
}
|
||||
request = new Request(packageName, userHandle, icon, appLabel, highBattery,
|
||||
badgedAppLabel, locationRequestFinishTime);
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public static class Request {
|
||||
public final String packageName;
|
||||
public final UserHandle userHandle;
|
||||
public final Drawable icon;
|
||||
public final CharSequence label;
|
||||
public final boolean isHighBattery;
|
||||
public final CharSequence contentDescription;
|
||||
public final long requestFinishTime;
|
||||
|
||||
public Request(String packageName, UserHandle userHandle, Drawable icon,
|
||||
CharSequence label, boolean isHighBattery, CharSequence contentDescription,
|
||||
long requestFinishTime) {
|
||||
this.packageName = packageName;
|
||||
this.userHandle = userHandle;
|
||||
this.icon = icon;
|
||||
this.label = label;
|
||||
this.isHighBattery = isHighBattery;
|
||||
this.contentDescription = contentDescription;
|
||||
this.requestFinishTime = requestFinishTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.settingslib.location;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.location.SettingInjectorService;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.SystemClock;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settingslib.R;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Adds the preferences specified by the {@link InjectedSetting} objects to a preference group.
|
||||
*
|
||||
* Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that
|
||||
* class directly because it is not a good match for our use case: we do not need the caching, and
|
||||
* so do not want the additional resource hit at app install/upgrade time; and we would have to
|
||||
* suppress the tie-breaking between multiple services reporting settings with the same name.
|
||||
* Code-sharing would require extracting {@link
|
||||
* android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources,
|
||||
* String, android.util.AttributeSet)} into an interface, which didn't seem worth it.
|
||||
*/
|
||||
public class SettingsInjector {
|
||||
static final String TAG = "SettingsInjector";
|
||||
|
||||
/**
|
||||
* If reading the status of a setting takes longer than this, we go ahead and start reading
|
||||
* the next setting.
|
||||
*/
|
||||
private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000;
|
||||
|
||||
/**
|
||||
* {@link Message#what} value for starting to load status values
|
||||
* in case we aren't already in the process of loading them.
|
||||
*/
|
||||
private static final int WHAT_RELOAD = 1;
|
||||
|
||||
/**
|
||||
* {@link Message#what} value sent after receiving a status message.
|
||||
*/
|
||||
private static final int WHAT_RECEIVED_STATUS = 2;
|
||||
|
||||
/**
|
||||
* {@link Message#what} value sent after the timeout waiting for a status message.
|
||||
*/
|
||||
private static final int WHAT_TIMEOUT = 3;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* The settings that were injected
|
||||
*/
|
||||
protected final Set<Setting> mSettings;
|
||||
|
||||
private final Handler mHandler;
|
||||
|
||||
public SettingsInjector(Context context) {
|
||||
mContext = context;
|
||||
mSettings = new HashSet<Setting>();
|
||||
mHandler = new StatusLoadingHandler(mSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list for a profile with one {@link InjectedSetting} object for each
|
||||
* {@link android.app.Service} that responds to
|
||||
* {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the expected setting
|
||||
* metadata.
|
||||
*
|
||||
* Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
|
||||
*
|
||||
* TODO: unit test
|
||||
*/
|
||||
protected List<InjectedSetting> getSettings(final UserHandle userHandle) {
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT);
|
||||
|
||||
final int profileId = userHandle.getIdentifier();
|
||||
List<ResolveInfo> resolveInfos =
|
||||
pm.queryIntentServicesAsUser(intent, PackageManager.GET_META_DATA, profileId);
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Found services for profile id " + profileId + ": " + resolveInfos);
|
||||
}
|
||||
|
||||
final PackageManager userPackageManager = mContext.createContextAsUser(
|
||||
userHandle, /* flags */ 0).getPackageManager();
|
||||
List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size());
|
||||
for (ResolveInfo resolveInfo : resolveInfos) {
|
||||
try {
|
||||
InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle,
|
||||
userPackageManager);
|
||||
if (setting == null) {
|
||||
Log.w(TAG, "Unable to load service info " + resolveInfo);
|
||||
} else {
|
||||
settings.add(setting);
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.w(TAG, "Unable to load service info " + resolveInfo, e);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Unable to load service info " + resolveInfo, e);
|
||||
}
|
||||
}
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the InjectedSetting information to a Preference object
|
||||
*/
|
||||
private void populatePreference(Preference preference, InjectedSetting setting) {
|
||||
preference.setTitle(setting.title);
|
||||
preference.setSummary(R.string.loading_injected_setting_summary);
|
||||
preference.setOnPreferenceClickListener(new ServiceSettingClickedListener(setting));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of preferences that other apps have injected.
|
||||
*
|
||||
* @param profileId Identifier of the user/profile to obtain the injected settings for or
|
||||
* UserHandle.USER_CURRENT for all profiles associated with current user.
|
||||
*/
|
||||
public Map<Integer, List<Preference>> getInjectedSettings(Context prefContext,
|
||||
final int profileId) {
|
||||
final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
|
||||
final List<UserHandle> profiles = um.getUserProfiles();
|
||||
final ArrayMap<Integer, List<Preference>> result = new ArrayMap<>();
|
||||
mSettings.clear();
|
||||
for (UserHandle userHandle : profiles) {
|
||||
if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) {
|
||||
final List<Preference> prefs = new ArrayList<>();
|
||||
Iterable<InjectedSetting> settings = getSettings(userHandle);
|
||||
for (InjectedSetting setting : settings) {
|
||||
Preference preference = createPreference(prefContext, setting);
|
||||
populatePreference(preference, setting);
|
||||
prefs.add(preference);
|
||||
mSettings.add(new Setting(setting, preference));
|
||||
}
|
||||
if (!prefs.isEmpty()) {
|
||||
result.put(userHandle.getIdentifier(), prefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reloadStatusMessages();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an injected Preference
|
||||
*
|
||||
* @return the created Preference
|
||||
*/
|
||||
protected Preference createPreference(Context prefContext, InjectedSetting setting) {
|
||||
return new Preference(prefContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives descendants a chance to log Preference click event
|
||||
*/
|
||||
protected void logPreferenceClick(Intent intent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the settings parsed from the attributes of the
|
||||
* {@link SettingInjectorService#META_DATA_NAME} tag, or null.
|
||||
*
|
||||
* Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
|
||||
*/
|
||||
private static InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle,
|
||||
PackageManager pm) throws XmlPullParserException, IOException {
|
||||
|
||||
ServiceInfo si = service.serviceInfo;
|
||||
ApplicationInfo ai = si.applicationInfo;
|
||||
|
||||
if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
|
||||
if (Log.isLoggable(TAG, Log.WARN)) {
|
||||
Log.w(TAG, "Ignoring attempt to inject setting from app not in system image: "
|
||||
+ service);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
XmlResourceParser parser = null;
|
||||
try {
|
||||
parser = si.loadXmlMetaData(pm, SettingInjectorService.META_DATA_NAME);
|
||||
if (parser == null) {
|
||||
throw new XmlPullParserException("No " + SettingInjectorService.META_DATA_NAME
|
||||
+ " meta-data for " + service + ": " + si);
|
||||
}
|
||||
|
||||
AttributeSet attrs = Xml.asAttributeSet(parser);
|
||||
|
||||
int type;
|
||||
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
|
||||
&& type != XmlPullParser.START_TAG) {
|
||||
}
|
||||
|
||||
String nodeName = parser.getName();
|
||||
if (!SettingInjectorService.ATTRIBUTES_NAME.equals(nodeName)) {
|
||||
throw new XmlPullParserException("Meta-data does not start with "
|
||||
+ SettingInjectorService.ATTRIBUTES_NAME + " tag");
|
||||
}
|
||||
|
||||
Resources res = pm.getResourcesForApplication(si.packageName);
|
||||
return parseAttributes(si.packageName, si.name, userHandle, res, attrs);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new XmlPullParserException(
|
||||
"Unable to load resources for package " + si.packageName);
|
||||
} finally {
|
||||
if (parser != null) {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an immutable representation of the static attributes for the setting, or null.
|
||||
*/
|
||||
private static InjectedSetting parseAttributes(String packageName, String className,
|
||||
UserHandle userHandle, Resources res, AttributeSet attrs) {
|
||||
|
||||
TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService);
|
||||
try {
|
||||
// Note that to help guard against malicious string injection, we do not allow dynamic
|
||||
// specification of the label (setting title)
|
||||
final String title = sa.getString(android.R.styleable.SettingInjectorService_title);
|
||||
final int iconId =
|
||||
sa.getResourceId(android.R.styleable.SettingInjectorService_icon, 0);
|
||||
final String settingsActivity =
|
||||
sa.getString(android.R.styleable.SettingInjectorService_settingsActivity);
|
||||
final String userRestriction = sa.getString(
|
||||
android.R.styleable.SettingInjectorService_userRestriction);
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "parsed title: " + title + ", iconId: " + iconId
|
||||
+ ", settingsActivity: " + settingsActivity);
|
||||
}
|
||||
return new InjectedSetting.Builder()
|
||||
.setPackageName(packageName)
|
||||
.setClassName(className)
|
||||
.setTitle(title)
|
||||
.setIconId(iconId)
|
||||
.setUserHandle(userHandle)
|
||||
.setSettingsActivity(settingsActivity)
|
||||
.setUserRestriction(userRestriction)
|
||||
.build();
|
||||
} finally {
|
||||
sa.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the status messages for all the preference items.
|
||||
*/
|
||||
public void reloadStatusMessages() {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "reloadingStatusMessages: " + mSettings);
|
||||
}
|
||||
mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD));
|
||||
}
|
||||
|
||||
protected class ServiceSettingClickedListener
|
||||
implements Preference.OnPreferenceClickListener {
|
||||
private InjectedSetting mInfo;
|
||||
|
||||
public ServiceSettingClickedListener(InjectedSetting info) {
|
||||
mInfo = info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
// Activity to start if they click on the preference.
|
||||
Intent settingIntent = new Intent();
|
||||
settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity);
|
||||
// No flags set to ensure the activity is launched within the same settings task.
|
||||
logPreferenceClick(settingIntent);
|
||||
mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the setting status values one at a time. Each load starts a subclass of {@link
|
||||
* SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
|
||||
* once.
|
||||
*/
|
||||
private static final class StatusLoadingHandler extends Handler {
|
||||
/**
|
||||
* References all the injected settings.
|
||||
*/
|
||||
WeakReference<Set<Setting>> mAllSettings;
|
||||
|
||||
/**
|
||||
* Settings whose status values need to be loaded. A set is used to prevent redundant loads.
|
||||
*/
|
||||
private Deque<Setting> mSettingsToLoad = new ArrayDeque<Setting>();
|
||||
|
||||
/**
|
||||
* Settings that are being loaded now and haven't timed out. In practice this should have
|
||||
* zero or one elements.
|
||||
*/
|
||||
private Set<Setting> mSettingsBeingLoaded = new ArraySet<Setting>();
|
||||
|
||||
public StatusLoadingHandler(Set<Setting> allSettings) {
|
||||
super(Looper.getMainLooper());
|
||||
mAllSettings = new WeakReference<>(allSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "handleMessage start: " + msg + ", " + this);
|
||||
}
|
||||
|
||||
// Update state in response to message
|
||||
switch (msg.what) {
|
||||
case WHAT_RELOAD: {
|
||||
final Set<Setting> allSettings = mAllSettings.get();
|
||||
if (allSettings != null) {
|
||||
// Reload requested, so must reload all settings
|
||||
mSettingsToLoad.clear();
|
||||
mSettingsToLoad.addAll(allSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WHAT_RECEIVED_STATUS:
|
||||
final Setting receivedSetting = (Setting) msg.obj;
|
||||
receivedSetting.maybeLogElapsedTime();
|
||||
mSettingsBeingLoaded.remove(receivedSetting);
|
||||
removeMessages(WHAT_TIMEOUT, receivedSetting);
|
||||
break;
|
||||
case WHAT_TIMEOUT:
|
||||
final Setting timedOutSetting = (Setting) msg.obj;
|
||||
mSettingsBeingLoaded.remove(timedOutSetting);
|
||||
if (Log.isLoggable(TAG, Log.WARN)) {
|
||||
Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime()
|
||||
+ " millis trying to get status for: " + timedOutSetting);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.wtf(TAG, "Unexpected what: " + msg);
|
||||
}
|
||||
|
||||
// Decide whether to load additional settings based on the new state. Start by seeing
|
||||
// if we have headroom to load another setting.
|
||||
if (mSettingsBeingLoaded.size() > 0) {
|
||||
// Don't load any more settings until one of the pending settings has completed.
|
||||
// To reduce memory pressure, we want to be loading at most one setting.
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, "too many services already live for " + msg + ", " + this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mSettingsToLoad.isEmpty()) {
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, "nothing left to do for " + msg + ", " + this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Setting setting = mSettingsToLoad.removeFirst();
|
||||
|
||||
// Request the status value
|
||||
setting.startService();
|
||||
mSettingsBeingLoaded.add(setting);
|
||||
|
||||
// Ensure that if receiving the status value takes too long, we start loading the
|
||||
// next value anyway
|
||||
Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
|
||||
sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "handleMessage end " + msg + ", " + this
|
||||
+ ", started loading " + setting);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatusLoadingHandler{" +
|
||||
"mSettingsToLoad=" + mSettingsToLoad +
|
||||
", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static class MessengerHandler extends Handler {
|
||||
private WeakReference<Setting> mSettingRef;
|
||||
private Handler mHandler;
|
||||
|
||||
public MessengerHandler(Setting setting, Handler handler) {
|
||||
mSettingRef = new WeakReference(setting);
|
||||
mHandler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
final Setting setting = mSettingRef.get();
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
Bundle bundle = msg.getData();
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
|
||||
}
|
||||
boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
|
||||
String summary = bundle.getString(SettingInjectorService.SUMMARY_KEY);
|
||||
final Preference preference = setting.preference;
|
||||
if (TextUtils.isEmpty(summary)) {
|
||||
// Set a placeholder summary when received empty summary from injected service.
|
||||
// This is necessary to avoid preference height change.
|
||||
preference.setSummary(R.string.summary_placeholder);
|
||||
} else {
|
||||
preference.setSummary(summary);
|
||||
}
|
||||
preference.setEnabled(enabled);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(WHAT_RECEIVED_STATUS, setting));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an injected setting and the corresponding preference.
|
||||
*/
|
||||
protected final class Setting {
|
||||
public final InjectedSetting setting;
|
||||
public final Preference preference;
|
||||
public long startMillis;
|
||||
|
||||
|
||||
public Setting(InjectedSetting setting, Preference preference) {
|
||||
this.setting = setting;
|
||||
this.preference = preference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Setting{" +
|
||||
"setting=" + setting +
|
||||
", preference=" + preference +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the service to fetch for the current status for the setting, and updates the
|
||||
* preference when the service replies.
|
||||
*/
|
||||
public void startService() {
|
||||
final ActivityManager am = (ActivityManager)
|
||||
mContext.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (!am.isUserRunning(setting.mUserHandle.getIdentifier())) {
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, "Cannot start service as user "
|
||||
+ setting.mUserHandle.getIdentifier() + " is not running");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Handler handler = new MessengerHandler(this, mHandler);
|
||||
Messenger messenger = new Messenger(handler);
|
||||
|
||||
Intent intent = setting.getServiceIntent();
|
||||
intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, setting + ": sending update intent: " + intent
|
||||
+ ", handler: " + handler);
|
||||
startMillis = SystemClock.elapsedRealtime();
|
||||
} else {
|
||||
startMillis = 0;
|
||||
}
|
||||
|
||||
// Start the service, making sure that this is attributed to the user associated with
|
||||
// the setting rather than the system user.
|
||||
mContext.startServiceAsUser(intent, setting.mUserHandle);
|
||||
}
|
||||
|
||||
public long getElapsedTime() {
|
||||
long end = SystemClock.elapsedRealtime();
|
||||
return end - startMillis;
|
||||
}
|
||||
|
||||
public void maybeLogElapsedTime() {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) {
|
||||
long elapsed = getElapsedTime();
|
||||
Log.d(TAG, this + " update took " + elapsed + " millis");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user