Have the NetworkScoreService bind to the scorer.
If the current active scorer provides a service that can handle the android.net.scoring.SCORE_NETWORKS action then the NetworkScoreService will bind to that service to keep the scorer alive. If no service is discovered then no attempt to bind will be made. BUG: 27612145 Change-Id: I3f6ed0cbd83e658f1533c3e37b0cac2692c01761
This commit is contained in:
@ -19,11 +19,9 @@ package android.net;
|
||||
import android.Manifest;
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.AppOpsManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.UserHandle;
|
||||
@ -69,12 +67,32 @@ public final class NetworkScorerAppManager {
|
||||
*/
|
||||
public final String mConfigurationActivityClassName;
|
||||
|
||||
/**
|
||||
* Optional class name of the scoring service we can bind to. Null if none is set.
|
||||
*/
|
||||
public final String mScoringServiceClassName;
|
||||
|
||||
public NetworkScorerAppData(String packageName, int packageUid, CharSequence scorerName,
|
||||
@Nullable String configurationActivityClassName) {
|
||||
@Nullable String configurationActivityClassName,
|
||||
@Nullable String scoringServiceClassName) {
|
||||
mScorerName = scorerName;
|
||||
mPackageName = packageName;
|
||||
mPackageUid = packageUid;
|
||||
mConfigurationActivityClassName = configurationActivityClassName;
|
||||
mScoringServiceClassName = scoringServiceClassName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("NetworkScorerAppData{");
|
||||
sb.append("mPackageName='").append(mPackageName).append('\'');
|
||||
sb.append(", mPackageUid=").append(mPackageUid);
|
||||
sb.append(", mScorerName=").append(mScorerName);
|
||||
sb.append(", mConfigurationActivityClassName='").append(mConfigurationActivityClassName)
|
||||
.append('\'');
|
||||
sb.append(", mScoringServiceClassName='").append(mScoringServiceClassName).append('\'');
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,18 +146,27 @@ public final class NetworkScorerAppManager {
|
||||
Intent intent = new Intent(NetworkScoreManager.ACTION_CUSTOM_ENABLE);
|
||||
intent.setPackage(receiverInfo.packageName);
|
||||
List<ResolveInfo> configActivities = pm.queryIntentActivities(intent, 0 /* flags */);
|
||||
if (!configActivities.isEmpty()) {
|
||||
if (configActivities != null && !configActivities.isEmpty()) {
|
||||
ActivityInfo activityInfo = configActivities.get(0).activityInfo;
|
||||
if (activityInfo != null) {
|
||||
configurationActivityClassName = activityInfo.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the scoring service class we can bind to, if any.
|
||||
String scoringServiceClassName = null;
|
||||
Intent serviceIntent = new Intent(NetworkScoreManager.ACTION_SCORE_NETWORKS);
|
||||
serviceIntent.setPackage(receiverInfo.packageName);
|
||||
ResolveInfo resolveServiceInfo = pm.resolveService(serviceIntent, 0 /* flags */);
|
||||
if (resolveServiceInfo != null && resolveServiceInfo.serviceInfo != null) {
|
||||
scoringServiceClassName = resolveServiceInfo.serviceInfo.name;
|
||||
}
|
||||
|
||||
// NOTE: loadLabel will attempt to load the receiver's label and fall back to the
|
||||
// app label if none is present.
|
||||
scorers.add(new NetworkScorerAppData(receiverInfo.packageName,
|
||||
receiverInfo.applicationInfo.uid, receiverInfo.loadLabel(pm),
|
||||
configurationActivityClassName));
|
||||
configurationActivityClassName, scoringServiceClassName));
|
||||
}
|
||||
|
||||
return scorers;
|
||||
|
@ -23,10 +23,10 @@ import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.net.NetworkScorerAppManager.NetworkScorerAppData;
|
||||
import android.os.UserHandle;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.Mock;
|
||||
@ -58,25 +58,26 @@ public class NetworkScorerAppManagerTest extends InstrumentationTestCase {
|
||||
|
||||
public void testGetAllValidScorers() throws Exception {
|
||||
// Package 1 - Valid scorer.
|
||||
Pair<ResolveInfo, ResolveInfo> package1 = buildResolveInfo("package1", 1, true, true,
|
||||
false);
|
||||
ResolveInfoHolder package1 = buildResolveInfo("package1", 1, true, true, false, false);
|
||||
|
||||
// Package 2 - Receiver does not have BROADCAST_NETWORK_PRIVILEGED permission.
|
||||
Pair<ResolveInfo, ResolveInfo> package2 = buildResolveInfo("package2", 2, false, true,
|
||||
false);
|
||||
ResolveInfoHolder package2 = buildResolveInfo("package2", 2, false, true, false, false);
|
||||
|
||||
// Package 3 - App does not have SCORE_NETWORKS permission.
|
||||
Pair<ResolveInfo, ResolveInfo> package3 = buildResolveInfo("package3", 3, true, false,
|
||||
false);
|
||||
ResolveInfoHolder package3 = buildResolveInfo("package3", 3, true, false, false, false);
|
||||
|
||||
// Package 4 - Valid scorer w/ optional config activity.
|
||||
Pair<ResolveInfo, ResolveInfo> package4 = buildResolveInfo("package4", 4, true, true, true);
|
||||
ResolveInfoHolder package4 = buildResolveInfo("package4", 4, true, true, true, false);
|
||||
|
||||
List<Pair<ResolveInfo, ResolveInfo>> scorers = new ArrayList<>();
|
||||
// Package 5 - Valid scorer w/ optional service to bind to.
|
||||
ResolveInfoHolder package5 = buildResolveInfo("package5", 5, true, true, false, true);
|
||||
|
||||
List<ResolveInfoHolder> scorers = new ArrayList<>();
|
||||
scorers.add(package1);
|
||||
scorers.add(package2);
|
||||
scorers.add(package3);
|
||||
scorers.add(package4);
|
||||
scorers.add(package5);
|
||||
setScorers(scorers);
|
||||
|
||||
Iterator<NetworkScorerAppData> result =
|
||||
@ -94,14 +95,20 @@ public class NetworkScorerAppManagerTest extends InstrumentationTestCase {
|
||||
assertEquals(4, next.mPackageUid);
|
||||
assertEquals(".ConfigActivity", next.mConfigurationActivityClassName);
|
||||
|
||||
assertTrue(result.hasNext());
|
||||
next = result.next();
|
||||
assertEquals("package5", next.mPackageName);
|
||||
assertEquals(5, next.mPackageUid);
|
||||
assertEquals(".ScoringService", next.mScoringServiceClassName);
|
||||
|
||||
assertFalse(result.hasNext());
|
||||
}
|
||||
|
||||
private void setScorers(List<Pair<ResolveInfo, ResolveInfo>> scorers) {
|
||||
private void setScorers(List<ResolveInfoHolder> scorers) {
|
||||
List<ResolveInfo> receivers = new ArrayList<>();
|
||||
for (final Pair<ResolveInfo, ResolveInfo> scorer : scorers) {
|
||||
receivers.add(scorer.first);
|
||||
if (scorer.second != null) {
|
||||
for (final ResolveInfoHolder scorer : scorers) {
|
||||
receivers.add(scorer.scorerResolveInfo);
|
||||
if (scorer.configActivityResolveInfo != null) {
|
||||
// This scorer has a config activity.
|
||||
Mockito.when(mMockPm.queryIntentActivities(
|
||||
Mockito.argThat(new ArgumentMatcher<Intent>() {
|
||||
@ -110,10 +117,26 @@ public class NetworkScorerAppManagerTest extends InstrumentationTestCase {
|
||||
Intent intent = (Intent) object;
|
||||
return NetworkScoreManager.ACTION_CUSTOM_ENABLE.equals(
|
||||
intent.getAction())
|
||||
&& scorer.first.activityInfo.packageName.equals(
|
||||
&& scorer.scorerResolveInfo.activityInfo.packageName.equals(
|
||||
intent.getPackage());
|
||||
}
|
||||
}), Mockito.eq(0))).thenReturn(Collections.singletonList(scorer.second));
|
||||
}), Mockito.eq(0))).thenReturn(
|
||||
Collections.singletonList(scorer.configActivityResolveInfo));
|
||||
}
|
||||
|
||||
if (scorer.serviceResolveInfo != null) {
|
||||
// This scorer has a service to bind to
|
||||
Mockito.when(mMockPm.resolveService(
|
||||
Mockito.argThat(new ArgumentMatcher<Intent>() {
|
||||
@Override
|
||||
public boolean matches(Object object) {
|
||||
Intent intent = (Intent) object;
|
||||
return NetworkScoreManager.ACTION_SCORE_NETWORKS.equals(
|
||||
intent.getAction())
|
||||
&& scorer.scorerResolveInfo.activityInfo.packageName.equals(
|
||||
intent.getPackage());
|
||||
}
|
||||
}), Mockito.eq(0))).thenReturn(scorer.serviceResolveInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,9 +151,9 @@ public class NetworkScorerAppManagerTest extends InstrumentationTestCase {
|
||||
.thenReturn(receivers);
|
||||
}
|
||||
|
||||
private Pair<ResolveInfo, ResolveInfo> buildResolveInfo(String packageName, int packageUid,
|
||||
boolean hasReceiverPermission, boolean hasScorePermission, boolean hasConfigActivity)
|
||||
throws Exception {
|
||||
private ResolveInfoHolder buildResolveInfo(String packageName, int packageUid,
|
||||
boolean hasReceiverPermission, boolean hasScorePermission, boolean hasConfigActivity,
|
||||
boolean hasServiceInfo) throws Exception {
|
||||
Mockito.when(mMockPm.checkPermission(permission.SCORE_NETWORKS, packageName))
|
||||
.thenReturn(hasScorePermission ?
|
||||
PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
|
||||
@ -150,6 +173,27 @@ public class NetworkScorerAppManagerTest extends InstrumentationTestCase {
|
||||
configActivityInfo.activityInfo = new ActivityInfo();
|
||||
configActivityInfo.activityInfo.name = ".ConfigActivity";
|
||||
}
|
||||
return Pair.create(resolveInfo, configActivityInfo);
|
||||
|
||||
ResolveInfo serviceInfo = null;
|
||||
if (hasServiceInfo) {
|
||||
serviceInfo = new ResolveInfo();
|
||||
serviceInfo.serviceInfo = new ServiceInfo();
|
||||
serviceInfo.serviceInfo.name = ".ScoringService";
|
||||
}
|
||||
|
||||
return new ResolveInfoHolder(resolveInfo, configActivityInfo, serviceInfo);
|
||||
}
|
||||
|
||||
private static class ResolveInfoHolder {
|
||||
final ResolveInfo scorerResolveInfo;
|
||||
final ResolveInfo configActivityResolveInfo;
|
||||
final ResolveInfo serviceResolveInfo;
|
||||
|
||||
public ResolveInfoHolder(ResolveInfo scorerResolveInfo,
|
||||
ResolveInfo configActivityResolveInfo, ResolveInfo serviceResolveInfo) {
|
||||
this.scorerResolveInfo = scorerResolveInfo;
|
||||
this.configActivityResolveInfo = configActivityResolveInfo;
|
||||
this.serviceResolveInfo = serviceResolveInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,12 @@ package com.android.server;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.INetworkScoreCache;
|
||||
import android.net.INetworkScoreService;
|
||||
@ -30,6 +32,7 @@ import android.net.NetworkScorerAppManager;
|
||||
import android.net.NetworkScorerAppManager.NetworkScorerAppData;
|
||||
import android.net.ScoredNetwork;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.PatternMatcher;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
@ -55,17 +58,17 @@ import java.util.Set;
|
||||
*/
|
||||
public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
private static final String TAG = "NetworkScoreService";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final Map<Integer, INetworkScoreCache> mScoreCaches;
|
||||
|
||||
/** Lock used to update mReceiver when scorer package changes occur. */
|
||||
private Object mReceiverLock = new Object[0];
|
||||
private final Object mReceiverLock = new Object[0];
|
||||
|
||||
/** Clears scores when the active scorer package is no longer valid. */
|
||||
@GuardedBy("mReceiverLock")
|
||||
private ScorerChangedReceiver mReceiver;
|
||||
private ScoringServiceConnection mServiceConnection;
|
||||
|
||||
private class ScorerChangedReceiver extends BroadcastReceiver {
|
||||
final String mRegisteredPackage;
|
||||
@ -77,14 +80,23 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if ((Intent.ACTION_PACKAGE_CHANGED.equals(action) ||
|
||||
Intent.ACTION_PACKAGE_REPLACED.equals(action) ||
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) &&
|
||||
NetworkScorerAppManager.getActiveScorer(mContext) == null) {
|
||||
// Package change has invalidated a scorer.
|
||||
Log.i(TAG, "Package " + mRegisteredPackage +
|
||||
" is no longer valid, disabling scoring");
|
||||
setScorerInternal(null);
|
||||
if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
|
||||
|| Intent.ACTION_PACKAGE_REPLACED.equals(action)
|
||||
|| Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
|
||||
NetworkScorerAppData activeScorer =
|
||||
NetworkScorerAppManager.getActiveScorer(mContext);
|
||||
if (activeScorer == null) {
|
||||
// Package change has invalidated a scorer.
|
||||
Log.i(TAG, "Package " + mRegisteredPackage +
|
||||
" is no longer valid, disabling scoring.");
|
||||
setScorerInternal(null);
|
||||
} else if (activeScorer.mScoringServiceClassName == null) {
|
||||
// The scoring service is not available, make sure it's unbound.
|
||||
unbindFromScoringServiceIfNeeded();
|
||||
} else {
|
||||
// The scoring service may have changed or been added.
|
||||
bindToScoringServiceIfNeeded(activeScorer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,6 +108,7 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
|
||||
/** Called when the system is ready to run third-party code but before it actually does so. */
|
||||
void systemReady() {
|
||||
if (DBG) Log.d(TAG, "systemReady");
|
||||
ContentResolver cr = mContext.getContentResolver();
|
||||
if (Settings.Global.getInt(cr, Settings.Global.NETWORK_SCORING_PROVISIONED, 0) == 0) {
|
||||
// On first run, we try to initialize the scorer to the one configured at build time.
|
||||
@ -111,7 +124,14 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
registerPackageReceiverIfNeeded();
|
||||
}
|
||||
|
||||
/** Called when the system is ready for us to start third-party code. */
|
||||
void systemRunning() {
|
||||
if (DBG) Log.d(TAG, "systemRunning");
|
||||
bindToScoringServiceIfNeeded();
|
||||
}
|
||||
|
||||
private void registerPackageReceiverIfNeeded() {
|
||||
if (DBG) Log.d(TAG, "registerPackageReceiverIfNeeded");
|
||||
NetworkScorerAppData scorer = NetworkScorerAppManager.getActiveScorer(mContext);
|
||||
synchronized (mReceiverLock) {
|
||||
// Unregister the receiver if the current scorer has changed since last registration.
|
||||
@ -142,6 +162,41 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
private void bindToScoringServiceIfNeeded() {
|
||||
if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded");
|
||||
NetworkScorerAppData scorerData = NetworkScorerAppManager.getActiveScorer(mContext);
|
||||
bindToScoringServiceIfNeeded(scorerData);
|
||||
}
|
||||
|
||||
private void bindToScoringServiceIfNeeded(NetworkScorerAppData scorerData) {
|
||||
if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + scorerData + ")");
|
||||
if (scorerData != null && scorerData.mScoringServiceClassName != null) {
|
||||
ComponentName componentName =
|
||||
new ComponentName(scorerData.mPackageName, scorerData.mScoringServiceClassName);
|
||||
// If we're connected to a different component then drop it.
|
||||
if (mServiceConnection != null
|
||||
&& !mServiceConnection.mComponentName.equals(componentName)) {
|
||||
unbindFromScoringServiceIfNeeded();
|
||||
}
|
||||
|
||||
// If we're not connected at all then create a new connection.
|
||||
if (mServiceConnection == null) {
|
||||
mServiceConnection = new ScoringServiceConnection(componentName);
|
||||
}
|
||||
|
||||
// Make sure the connection is connected (idempotent)
|
||||
mServiceConnection.connect(mContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void unbindFromScoringServiceIfNeeded() {
|
||||
if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
|
||||
if (mServiceConnection != null) {
|
||||
mServiceConnection.disconnect(mContext);
|
||||
}
|
||||
mServiceConnection = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateScores(ScoredNetwork[] networks) {
|
||||
if (!NetworkScorerAppManager.isCallerActiveScorer(mContext, getCallingUid())) {
|
||||
@ -228,8 +283,10 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
|
||||
/** Set the active scorer. Callers are responsible for checking permissions as appropriate. */
|
||||
private boolean setScorerInternal(String packageName) {
|
||||
if (DBG) Log.d(TAG, "setScorerInternal(" + packageName + ")");
|
||||
long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
unbindFromScoringServiceIfNeeded();
|
||||
// Preemptively clear scores even though the set operation could fail. We do this for
|
||||
// safety as scores should never be compared across apps; in practice, Settings should
|
||||
// only be allowing valid apps to be set as scorers, so failure here should be rare.
|
||||
@ -237,8 +294,13 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
// Get the scorer that is about to be replaced, if any, so we can notify it directly.
|
||||
NetworkScorerAppData prevScorer = NetworkScorerAppManager.getActiveScorer(mContext);
|
||||
boolean result = NetworkScorerAppManager.setActiveScorer(mContext, packageName);
|
||||
// Unconditionally attempt to bind to the current scorer. If setActiveScorer() failed
|
||||
// then we'll attempt to restore the previous binding (if any), otherwise an attempt
|
||||
// will be made to bind to the new scorer.
|
||||
bindToScoringServiceIfNeeded();
|
||||
if (result) { // new scorer successfully set
|
||||
registerPackageReceiverIfNeeded();
|
||||
|
||||
Intent intent = new Intent(NetworkScoreManager.ACTION_SCORER_CHANGED);
|
||||
if (prevScorer != null) { // Directly notify the old scorer.
|
||||
intent.setPackage(prevScorer.mPackageName);
|
||||
@ -295,7 +357,6 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
return;
|
||||
}
|
||||
writer.println("Current scorer: " + currentScorer.mPackageName);
|
||||
writer.flush();
|
||||
|
||||
for (INetworkScoreCache scoreCache : getScoreCaches()) {
|
||||
try {
|
||||
@ -307,6 +368,12 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mServiceConnection != null) {
|
||||
mServiceConnection.dump(fd, writer, args);
|
||||
} else {
|
||||
writer.println("ScoringServiceConnection: null");
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,4 +387,50 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
|
||||
return new HashSet<>(mScoreCaches.values());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ScoringServiceConnection implements ServiceConnection {
|
||||
private final ComponentName mComponentName;
|
||||
private boolean mBound = false;
|
||||
|
||||
ScoringServiceConnection(ComponentName componentName) {
|
||||
mComponentName = componentName;
|
||||
}
|
||||
|
||||
void connect(Context context) {
|
||||
disconnect(context);
|
||||
Intent service = new Intent();
|
||||
service.setComponent(mComponentName);
|
||||
mBound = context.bindServiceAsUser(service, this,
|
||||
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
|
||||
UserHandle.SYSTEM);
|
||||
if (!mBound) {
|
||||
Log.w(TAG, "Bind call failed for " + service);
|
||||
}
|
||||
}
|
||||
|
||||
void disconnect(Context context) {
|
||||
try {
|
||||
if (mBound) {
|
||||
mBound = false;
|
||||
context.unbindService(this);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Unbind failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
if (DBG) Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
|
||||
}
|
||||
|
||||
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
|
||||
writer.println("ScoringServiceConnection: " + mComponentName + ", bound: " + mBound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1423,6 +1423,12 @@ public final class SystemServer {
|
||||
} catch (Throwable e) {
|
||||
reportWtf("Notifying MmsService running", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (networkScoreF != null) networkScoreF.systemRunning();
|
||||
} catch (Throwable e) {
|
||||
reportWtf("Notifying NetworkScoreService running", e);
|
||||
}
|
||||
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user