1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.telephony.metrics; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 22 import com.android.internal.annotations.VisibleForTesting; 23 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms; 24 import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage; 25 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession; 26 import com.android.telephony.Rlog; 27 28 import java.io.FileOutputStream; 29 import java.io.IOException; 30 import java.nio.file.Files; 31 import java.security.SecureRandom; 32 import java.util.Arrays; 33 34 /** 35 * Stores and aggregates metrics that should not be pulled at arbitrary frequency. 36 * 37 * <p>NOTE: while this class checks timestamp against {@code minIntervalMillis}, it is {@link 38 * MetricsCollector}'s responsibility to ensure {@code minIntervalMillis} is set correctly. 39 */ 40 public class PersistAtomsStorage { 41 private static final String TAG = PersistAtomsStorage.class.getSimpleName(); 42 43 /** Name of the file where cached statistics are saved to. */ 44 private static final String FILENAME = "persist_atoms.pb"; 45 46 /** Maximum number of call sessions to store during pulls. */ 47 private static final int MAX_NUM_CALL_SESSIONS = 50; 48 49 /** Stores persist atoms and persist states of the puller. */ 50 @VisibleForTesting protected final PersistAtoms mAtoms; 51 52 /** Aggregates RAT duration and call count. */ 53 private final VoiceCallRatTracker mVoiceCallRatTracker; 54 55 private final Context mContext; 56 private static final SecureRandom sRandom = new SecureRandom(); 57 PersistAtomsStorage(Context context)58 public PersistAtomsStorage(Context context) { 59 mContext = context; 60 mAtoms = loadAtomsFromFile(); 61 mVoiceCallRatTracker = VoiceCallRatTracker.fromProto(mAtoms.rawVoiceCallRatUsage); 62 } 63 64 /** Adds a call to the storage. */ addVoiceCallSession(VoiceCallSession call)65 public synchronized void addVoiceCallSession(VoiceCallSession call) { 66 int newLength = mAtoms.voiceCallSession.length + 1; 67 if (newLength > MAX_NUM_CALL_SESSIONS) { 68 // will evict one previous call randomly instead of making the array larger 69 newLength = MAX_NUM_CALL_SESSIONS; 70 } else { 71 mAtoms.voiceCallSession = Arrays.copyOf(mAtoms.voiceCallSession, newLength); 72 } 73 int insertAt = 0; 74 if (newLength > 1) { 75 // shuffle when each call is added, or randomly replace a previous call instead if 76 // MAX_NUM_CALL_SESSIONS is reached (call at the last index is evicted). 77 insertAt = sRandom.nextInt(newLength); 78 mAtoms.voiceCallSession[newLength - 1] = mAtoms.voiceCallSession[insertAt]; 79 } 80 mAtoms.voiceCallSession[insertAt] = call; 81 saveAtomsToFile(); 82 } 83 84 /** Adds RAT usages to the storage when a call session ends. */ addVoiceCallRatUsage(VoiceCallRatTracker ratUsages)85 public synchronized void addVoiceCallRatUsage(VoiceCallRatTracker ratUsages) { 86 mVoiceCallRatTracker.mergeWith(ratUsages); 87 mAtoms.rawVoiceCallRatUsage = mVoiceCallRatTracker.toProto(); 88 saveAtomsToFile(); 89 } 90 91 /** 92 * Returns and clears the voice call sessions if last pulled longer than {@code 93 * minIntervalMillis} ago, otherwise returns {@code null}. 94 */ 95 @Nullable getVoiceCallSessions(long minIntervalMillis)96 public synchronized VoiceCallSession[] getVoiceCallSessions(long minIntervalMillis) { 97 if (getWallTimeMillis() - mAtoms.voiceCallSessionPullTimestampMillis > minIntervalMillis) { 98 mAtoms.voiceCallSessionPullTimestampMillis = getWallTimeMillis(); 99 VoiceCallSession[] previousCalls = mAtoms.voiceCallSession; 100 mAtoms.voiceCallSession = new VoiceCallSession[0]; 101 saveAtomsToFile(); 102 return previousCalls; 103 } else { 104 return null; 105 } 106 } 107 108 /** 109 * Returns and clears the voice call RAT usages if last pulled longer than {@code 110 * minIntervalMillis} ago, otherwise returns {@code null}. 111 */ 112 @Nullable getVoiceCallRatUsages(long minIntervalMillis)113 public synchronized RawVoiceCallRatUsage[] getVoiceCallRatUsages(long minIntervalMillis) { 114 if (getWallTimeMillis() - mAtoms.rawVoiceCallRatUsagePullTimestampMillis 115 > minIntervalMillis) { 116 mAtoms.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis(); 117 RawVoiceCallRatUsage[] previousUsages = mAtoms.rawVoiceCallRatUsage; 118 mVoiceCallRatTracker.clear(); 119 mAtoms.rawVoiceCallRatUsage = new RawVoiceCallRatUsage[0]; 120 saveAtomsToFile(); 121 return previousUsages; 122 } else { 123 return null; 124 } 125 } 126 127 /** Loads {@link PersistAtoms} from a file in private storage. */ loadAtomsFromFile()128 private PersistAtoms loadAtomsFromFile() { 129 try { 130 PersistAtoms atomsFromFile = 131 PersistAtoms.parseFrom( 132 Files.readAllBytes(mContext.getFileStreamPath(FILENAME).toPath())); 133 // check all the fields in case of situations such as OTA or crash during saving 134 if (atomsFromFile.rawVoiceCallRatUsage == null) { 135 atomsFromFile.rawVoiceCallRatUsage = new RawVoiceCallRatUsage[0]; 136 } 137 if (atomsFromFile.voiceCallSession == null) { 138 atomsFromFile.voiceCallSession = new VoiceCallSession[0]; 139 } 140 if (atomsFromFile.voiceCallSession.length > MAX_NUM_CALL_SESSIONS) { 141 atomsFromFile.voiceCallSession = 142 Arrays.copyOf(atomsFromFile.voiceCallSession, MAX_NUM_CALL_SESSIONS); 143 } 144 // out of caution, set timestamps to now if they are missing 145 if (atomsFromFile.rawVoiceCallRatUsagePullTimestampMillis == 0L) { 146 atomsFromFile.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis(); 147 } 148 if (atomsFromFile.voiceCallSessionPullTimestampMillis == 0L) { 149 atomsFromFile.voiceCallSessionPullTimestampMillis = getWallTimeMillis(); 150 } 151 return atomsFromFile; 152 } catch (IOException | NullPointerException e) { 153 Rlog.e(TAG, "cannot load/parse PersistAtoms", e); 154 return makeNewPersistAtoms(); 155 } 156 } 157 158 /** Saves a copy of {@link PersistAtoms} to a file in private storage. */ saveAtomsToFile()159 private void saveAtomsToFile() { 160 try (FileOutputStream stream = mContext.openFileOutput(FILENAME, Context.MODE_PRIVATE)) { 161 stream.write(PersistAtoms.toByteArray(mAtoms)); 162 } catch (IOException e) { 163 Rlog.e(TAG, "cannot save PersistAtoms", e); 164 } 165 } 166 167 /** Returns an empty PersistAtoms with pull timestamp set to current time. */ makeNewPersistAtoms()168 private PersistAtoms makeNewPersistAtoms() { 169 PersistAtoms atoms = new PersistAtoms(); 170 // allow pulling only after some time so data are sufficiently aggregated 171 atoms.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis(); 172 atoms.voiceCallSessionPullTimestampMillis = getWallTimeMillis(); 173 Rlog.d(TAG, "created new PersistAtoms"); 174 return atoms; 175 } 176 177 @VisibleForTesting getWallTimeMillis()178 protected long getWallTimeMillis() { 179 // epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP 180 return System.currentTimeMillis(); 181 } 182 } 183