Implement BatteryUsageStatsStore
Bug: 187223764
Test: atest FrameworksCoreTests:BatteryUsageStatsStoreTest
Change-Id: I66bbb173f402e795c900ccf3bc82b8fb210142a4
Merged-In: I66bbb173f402e795c900ccf3bc82b8fb210142a4
(cherry picked from commit 2a0460f701)
This commit is contained in:
@@ -341,6 +341,19 @@ public class BatteryStatsImpl extends BatteryStats {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for the battery stats reset.
|
||||
*/
|
||||
public interface BatteryResetListener {
|
||||
|
||||
/**
|
||||
* Callback invoked immediately prior to resetting battery stats.
|
||||
*/
|
||||
void prepareForBatteryStatsReset();
|
||||
}
|
||||
|
||||
private BatteryResetListener mBatteryResetListener;
|
||||
|
||||
public interface BatteryCallback {
|
||||
public void batteryNeedsCpuUpdate();
|
||||
public void batteryPowerChanged(boolean onBattery);
|
||||
@@ -11184,6 +11197,10 @@ public class BatteryStatsImpl extends BatteryStats {
|
||||
mDischargeCounter.reset(false, elapsedRealtimeUs);
|
||||
}
|
||||
|
||||
public void setBatteryResetListener(BatteryResetListener batteryResetListener) {
|
||||
mBatteryResetListener = batteryResetListener;
|
||||
}
|
||||
|
||||
public void resetAllStatsCmdLocked() {
|
||||
final long mSecUptime = mClocks.uptimeMillis();
|
||||
long uptimeUs = mSecUptime * 1000;
|
||||
@@ -11219,6 +11236,10 @@ public class BatteryStatsImpl extends BatteryStats {
|
||||
}
|
||||
|
||||
private void resetAllStatsLocked(long uptimeMillis, long elapsedRealtimeMillis) {
|
||||
if (mBatteryResetListener != null) {
|
||||
mBatteryResetListener.prepareForBatteryStatsReset();
|
||||
}
|
||||
|
||||
final long uptimeUs = uptimeMillis * 1000;
|
||||
final long elapsedRealtimeUs = elapsedRealtimeMillis * 1000;
|
||||
mStartCount = 0;
|
||||
|
||||
231
core/java/com/android/internal/os/BatteryUsageStatsStore.java
Normal file
231
core/java/com/android/internal/os/BatteryUsageStatsStore.java
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.internal.os;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.os.BatteryUsageStats;
|
||||
import android.os.BatteryUsageStatsQuery;
|
||||
import android.os.Handler;
|
||||
import android.util.LongArray;
|
||||
import android.util.Slog;
|
||||
import android.util.TypedXmlPullParser;
|
||||
import android.util.TypedXmlSerializer;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* A storage mechanism for BatteryUsageStats snapshots.
|
||||
*/
|
||||
public class BatteryUsageStatsStore {
|
||||
private static final String TAG = "BatteryUsageStatsStore";
|
||||
|
||||
private static final List<BatteryUsageStatsQuery> BATTERY_USAGE_STATS_QUERY = List.of(
|
||||
new BatteryUsageStatsQuery.Builder()
|
||||
.setMaxStatsAgeMs(0)
|
||||
.includePowerModels()
|
||||
.build());
|
||||
private static final String BATTERY_USAGE_STATS_DIR = "battery-usage-stats";
|
||||
private static final String SNAPSHOT_FILE_EXTENSION = ".bus";
|
||||
private static final String DIR_LOCK_FILENAME = ".lock";
|
||||
private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024;
|
||||
|
||||
private final Context mContext;
|
||||
private final BatteryStatsImpl mBatteryStats;
|
||||
private final File mStoreDir;
|
||||
private final File mLockFile;
|
||||
private final long mMaxStorageBytes;
|
||||
private final Handler mHandler;
|
||||
private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
|
||||
|
||||
public BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir,
|
||||
Handler handler) {
|
||||
this(context, stats, systemDir, handler, MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir,
|
||||
Handler handler, long maxStorageBytes) {
|
||||
mContext = context;
|
||||
mBatteryStats = batteryStats;
|
||||
mStoreDir = new File(systemDir, BATTERY_USAGE_STATS_DIR);
|
||||
mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME);
|
||||
mHandler = handler;
|
||||
mMaxStorageBytes = maxStorageBytes;
|
||||
mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset);
|
||||
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats);
|
||||
}
|
||||
|
||||
private void prepareForBatteryStatsReset() {
|
||||
final List<BatteryUsageStats> stats =
|
||||
mBatteryUsageStatsProvider.getBatteryUsageStats(BATTERY_USAGE_STATS_QUERY);
|
||||
if (stats.isEmpty()) {
|
||||
Slog.wtf(TAG, "No battery usage stats generated");
|
||||
return;
|
||||
}
|
||||
|
||||
mHandler.post(() -> storeBatteryUsageStats(stats.get(0)));
|
||||
}
|
||||
|
||||
private void storeBatteryUsageStats(BatteryUsageStats stats) {
|
||||
try (FileLock lock = lockSnapshotDirectory()) {
|
||||
if (!mStoreDir.exists()) {
|
||||
if (!mStoreDir.mkdirs()) {
|
||||
Slog.e(TAG,
|
||||
"Could not create a directory for battery usage stats snapshots");
|
||||
return;
|
||||
}
|
||||
}
|
||||
File file = makeSnapshotFilename(stats.getStatsEndTimestamp());
|
||||
try {
|
||||
writeXmlFileLocked(stats, file);
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "Cannot save battery usage stats", e);
|
||||
}
|
||||
|
||||
removeOldSnapshotsLocked();
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Cannot lock battery usage stats directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps of the stored BatteryUsageStats snapshots. The timestamp corresponds
|
||||
* to the time the snapshot was taken {@link BatteryUsageStats#getStatsEndTimestamp()}.
|
||||
*/
|
||||
public long[] listBatteryUsageStatsTimestamps() {
|
||||
LongArray timestamps = new LongArray(100);
|
||||
try (FileLock lock = lockSnapshotDirectory()) {
|
||||
for (File file : mStoreDir.listFiles()) {
|
||||
String fileName = file.getName();
|
||||
if (fileName.endsWith(SNAPSHOT_FILE_EXTENSION)) {
|
||||
try {
|
||||
String fileNameWithoutExtension = fileName.substring(0,
|
||||
fileName.length() - SNAPSHOT_FILE_EXTENSION.length());
|
||||
timestamps.add(Long.parseLong(fileNameWithoutExtension));
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.wtf(TAG, "Invalid format of BatteryUsageStats snapshot file name: "
|
||||
+ fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Cannot lock battery usage stats directory", e);
|
||||
}
|
||||
return timestamps.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the specified snapshot of BatteryUsageStats. Returns null if the snapshot
|
||||
* does not exist.
|
||||
*/
|
||||
@Nullable
|
||||
public BatteryUsageStats loadBatteryUsageStats(long timestamp) {
|
||||
try (FileLock lock = lockSnapshotDirectory()) {
|
||||
File file = makeSnapshotFilename(timestamp);
|
||||
try {
|
||||
return readXmlFileLocked(file);
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "Cannot read battery usage stats", e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Cannot lock battery usage stats directory", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileLock lockSnapshotDirectory() throws IOException {
|
||||
mLockFile.getParentFile().mkdirs();
|
||||
mLockFile.createNewFile();
|
||||
return FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file name by formatting the timestamp as 19-digit zero-padded number.
|
||||
* This ensures that sorted directory list follows the chronological order.
|
||||
*/
|
||||
private File makeSnapshotFilename(long statsEndTimestamp) {
|
||||
return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", statsEndTimestamp)
|
||||
+ SNAPSHOT_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
private void writeXmlFileLocked(BatteryUsageStats stats, File file) throws IOException {
|
||||
try (OutputStream out = new FileOutputStream(file)) {
|
||||
TypedXmlSerializer serializer = Xml.newBinarySerializer();
|
||||
serializer.setOutput(out, StandardCharsets.UTF_8.name());
|
||||
serializer.startDocument(null, true);
|
||||
stats.writeXml(serializer);
|
||||
serializer.endDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private BatteryUsageStats readXmlFileLocked(File file)
|
||||
throws IOException, XmlPullParserException {
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
TypedXmlPullParser parser = Xml.newBinaryPullParser();
|
||||
parser.setInput(in, StandardCharsets.UTF_8.name());
|
||||
return BatteryUsageStats.createFromXml(parser);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeOldSnapshotsLocked() {
|
||||
// Read the directory list into a _sorted_ map. The alphanumeric ordering
|
||||
// corresponds to the historical order of snapshots because the file names
|
||||
// are timestamps zero-padded to the same length.
|
||||
long totalSize = 0;
|
||||
TreeMap<File, Long> mFileSizes = new TreeMap<>();
|
||||
for (File file : mStoreDir.listFiles()) {
|
||||
final long fileSize = file.length();
|
||||
totalSize += fileSize;
|
||||
if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) {
|
||||
mFileSizes.put(file, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
while (totalSize > mMaxStorageBytes) {
|
||||
final Map.Entry<File, Long> entry = mFileSizes.firstEntry();
|
||||
if (entry == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
File file = entry.getKey();
|
||||
if (!file.delete()) {
|
||||
Slog.e(TAG, "Cannot delete battery usage stats " + file);
|
||||
}
|
||||
totalSize -= entry.getValue();
|
||||
mFileSizes.remove(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import org.junit.runners.Suite;
|
||||
BatteryStatsUidTest.class,
|
||||
BatteryUsageStatsProviderTest.class,
|
||||
BatteryUsageStatsTest.class,
|
||||
BatteryUsageStatsStoreTest.class,
|
||||
BatteryStatsUserLifecycleTests.class,
|
||||
BluetoothPowerCalculatorTest.class,
|
||||
BstatsCpuTimesValidationTest.class,
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.internal.os;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.BatteryUsageStats;
|
||||
import android.os.BatteryUsageStatsQuery;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.TypedXmlSerializer;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class BatteryUsageStatsStoreTest {
|
||||
private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 2 * 1024;
|
||||
|
||||
private final MockClocks mMockClocks = new MockClocks();
|
||||
private MockBatteryStatsImpl mBatteryStats;
|
||||
private BatteryUsageStatsStore mBatteryUsageStatsStore;
|
||||
private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
|
||||
private File mStoreDirectory;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mMockClocks.currentTime = 123;
|
||||
mBatteryStats = new MockBatteryStatsImpl(mMockClocks);
|
||||
mBatteryStats.setNoAutoReset(true);
|
||||
|
||||
Context context = InstrumentationRegistry.getContext();
|
||||
|
||||
mStoreDirectory = new File(context.getCacheDir(), "BatteryUsageStatsStoreTest");
|
||||
clearDirectory(mStoreDirectory);
|
||||
|
||||
mBatteryUsageStatsStore = new BatteryUsageStatsStore(context, mBatteryStats,
|
||||
mStoreDirectory, new TestHandler(), MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
|
||||
|
||||
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mBatteryStats);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreSnapshot() {
|
||||
mMockClocks.currentTime = 1_600_000;
|
||||
|
||||
prepareBatteryStats();
|
||||
mBatteryStats.resetAllStatsCmdLocked();
|
||||
|
||||
final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
|
||||
assertThat(timestamps).hasLength(1);
|
||||
assertThat(timestamps[0]).isEqualTo(1_600_000);
|
||||
|
||||
final BatteryUsageStats batteryUsageStats = mBatteryUsageStatsStore.loadBatteryUsageStats(
|
||||
1_600_000);
|
||||
assertThat(batteryUsageStats.getStatsStartTimestamp()).isEqualTo(123);
|
||||
assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(1_600_000);
|
||||
assertThat(batteryUsageStats.getBatteryCapacity()).isEqualTo(4000);
|
||||
assertThat(batteryUsageStats.getDischargePercentage()).isEqualTo(5);
|
||||
assertThat(batteryUsageStats.getAggregateBatteryConsumer(
|
||||
BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE).getConsumedPower())
|
||||
.isEqualTo(600); // (3_600_000 - 3_000_000) / 1000
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGarbageCollectOldSnapshots() throws Exception {
|
||||
prepareBatteryStats();
|
||||
|
||||
mMockClocks.realtime = 10_000_000;
|
||||
mMockClocks.uptime = 10_000_000;
|
||||
mMockClocks.currentTime = 10_000_000;
|
||||
|
||||
final int snapshotFileSize = getSnapshotFileSize();
|
||||
final int numberOfSnapshots =
|
||||
(int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / snapshotFileSize);
|
||||
for (int i = 0; i < numberOfSnapshots + 2; i++) {
|
||||
mBatteryStats.resetAllStatsCmdLocked();
|
||||
|
||||
mMockClocks.realtime += 10_000_000;
|
||||
mMockClocks.uptime += 10_000_000;
|
||||
mMockClocks.currentTime += 10_000_000;
|
||||
prepareBatteryStats();
|
||||
}
|
||||
|
||||
final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
|
||||
Arrays.sort(timestamps);
|
||||
assertThat(timestamps).hasLength(numberOfSnapshots);
|
||||
// Two snapshots (10_000_000 and 20_000_000) should have been discarded
|
||||
assertThat(timestamps[0]).isEqualTo(30_000_000);
|
||||
assertThat(getDirectorySize(mStoreDirectory))
|
||||
.isAtMost(MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
|
||||
}
|
||||
|
||||
private void prepareBatteryStats() {
|
||||
mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
|
||||
/* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
|
||||
mMockClocks.realtime, mMockClocks.uptime, mMockClocks.currentTime);
|
||||
mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
|
||||
/* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
|
||||
mMockClocks.realtime + 500_000, mMockClocks.uptime + 500_000,
|
||||
mMockClocks.currentTime + 500_000);
|
||||
}
|
||||
|
||||
private void clearDirectory(File dir) {
|
||||
if (dir.exists()) {
|
||||
for (File child : dir.listFiles()) {
|
||||
if (child.isDirectory()) {
|
||||
clearDirectory(child);
|
||||
}
|
||||
child.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long getDirectorySize(File dir) {
|
||||
long size = 0;
|
||||
if (dir.exists()) {
|
||||
for (File child : dir.listFiles()) {
|
||||
if (child.isDirectory()) {
|
||||
size += getDirectorySize(child);
|
||||
} else {
|
||||
size += child.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
private int getSnapshotFileSize() throws IOException {
|
||||
BatteryUsageStats stats = mBatteryUsageStatsProvider.getBatteryUsageStats(
|
||||
new BatteryUsageStatsQuery.Builder()
|
||||
.setMaxStatsAgeMs(0)
|
||||
.includePowerModels()
|
||||
.build());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
TypedXmlSerializer serializer = Xml.newBinarySerializer();
|
||||
serializer.setOutput(out, StandardCharsets.UTF_8.name());
|
||||
serializer.startDocument(null, true);
|
||||
stats.writeXml(serializer);
|
||||
serializer.endDocument();
|
||||
return out.toByteArray().length;
|
||||
}
|
||||
|
||||
private static class TestHandler extends Handler {
|
||||
TestHandler() {
|
||||
super(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
|
||||
msg.getCallback().run();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ import com.android.internal.os.BackgroundThread;
|
||||
import com.android.internal.os.BatteryStatsHelper;
|
||||
import com.android.internal.os.BatteryStatsImpl;
|
||||
import com.android.internal.os.BatteryUsageStatsProvider;
|
||||
import com.android.internal.os.BatteryUsageStatsStore;
|
||||
import com.android.internal.os.BinderCallsStats;
|
||||
import com.android.internal.os.PowerProfile;
|
||||
import com.android.internal.os.RailStats;
|
||||
@@ -124,10 +125,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub
|
||||
Watchdog.Monitor {
|
||||
static final String TAG = "BatteryStatsService";
|
||||
static final boolean DBG = false;
|
||||
private static final boolean BATTERY_USAGE_STORE_ENABLED = false;
|
||||
|
||||
private static IBatteryStats sService;
|
||||
|
||||
final BatteryStatsImpl mStats;
|
||||
private final BatteryUsageStatsStore mBatteryUsageStatsStore;
|
||||
private final BatteryStatsImpl.UserInfoProvider mUserManagerUserInfoProvider;
|
||||
private final Context mContext;
|
||||
private final BatteryExternalStatsWorker mWorker;
|
||||
@@ -341,6 +344,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub
|
||||
|
||||
mStats = new BatteryStatsImpl(systemDir, handler, this,
|
||||
this, mUserManagerUserInfoProvider);
|
||||
if (BATTERY_USAGE_STORE_ENABLED) {
|
||||
mBatteryUsageStatsStore =
|
||||
new BatteryUsageStatsStore(context, mStats, systemDir, mHandler);
|
||||
} else {
|
||||
mBatteryUsageStatsStore = null;
|
||||
}
|
||||
mWorker = new BatteryExternalStatsWorker(context, mStats);
|
||||
mStats.setExternalStatsSyncLocked(mWorker);
|
||||
mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
|
||||
|
||||
Reference in New Issue
Block a user