1 /*
2  * Copyright (C) 2022 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 android.net.netstats;
18 
19 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
20 import static android.net.ConnectivityManager.TYPE_MOBILE;
21 import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
22 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
23 import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
24 import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
25 import static android.net.NetworkStats.SET_DEFAULT;
26 import static android.net.NetworkStats.TAG_NONE;
27 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
28 
29 import android.annotation.NonNull;
30 import android.annotation.StringDef;
31 import android.annotation.SystemApi;
32 import android.net.NetworkIdentity;
33 import android.net.NetworkStatsCollection;
34 import android.net.NetworkStatsHistory;
35 import android.net.NetworkTemplate;
36 import android.os.Environment;
37 import android.util.AtomicFile;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.util.ArtFastDataInput;
41 
42 import libcore.io.IoUtils;
43 
44 import java.io.BufferedInputStream;
45 import java.io.DataInput;
46 import java.io.DataInputStream;
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.FileNotFoundException;
50 import java.io.IOException;
51 import java.lang.annotation.Retention;
52 import java.lang.annotation.RetentionPolicy;
53 import java.net.ProtocolException;
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.HashSet;
57 import java.util.Map;
58 import java.util.Set;
59 
60 /**
61  * Helper class to read old version of persistent network statistics.
62  *
63  * The implementation is intended to be modified by OEM partners to
64  * accommodate their custom changes.
65  *
66  * @hide
67  */
68 @SystemApi(client = MODULE_LIBRARIES)
69 public class NetworkStatsDataMigrationUtils {
70     /**
71      * Prefix of the files which are used to store per network interface statistics.
72      */
73     public static final String PREFIX_XT = "xt";
74     /**
75      * Prefix of the files which are used to store per uid statistics.
76      */
77     public static final String PREFIX_UID = "uid";
78     /**
79      * Prefix of the files which are used to store per uid tagged traffic statistics.
80      */
81     public static final String PREFIX_UID_TAG = "uid_tag";
82 
83     /** @hide */
84     @StringDef(prefix = {"PREFIX_"}, value = {
85         PREFIX_XT,
86         PREFIX_UID,
87         PREFIX_UID_TAG,
88     })
89     @Retention(RetentionPolicy.SOURCE)
90     public @interface Prefix {}
91 
92     private static final Map<String, String> sPrefixLegacyFileNameMap = Map.of(
93             PREFIX_XT, "netstats_xt.bin",
94             PREFIX_UID, "netstats_uid.bin",
95             PREFIX_UID_TAG, "netstats_uid.bin");
96 
97     // These version constants are copied from NetworkStatsCollection/History, which is okay for
98     // OEMs to modify to adapt their own logic.
99     private static class CollectionVersion {
100         static final int VERSION_NETWORK_INIT = 1;
101 
102         static final int VERSION_UID_INIT = 1;
103         static final int VERSION_UID_WITH_IDENT = 2;
104         static final int VERSION_UID_WITH_TAG = 3;
105         static final int VERSION_UID_WITH_SET = 4;
106 
107         static final int VERSION_UNIFIED_INIT = 16;
108     }
109 
110     private static class HistoryVersion {
111         static final int VERSION_INIT = 1;
112         static final int VERSION_ADD_PACKETS = 2;
113         static final int VERSION_ADD_ACTIVE = 3;
114     }
115 
116     private static class IdentitySetVersion {
117         static final int VERSION_INIT = 1;
118         static final int VERSION_ADD_ROAMING = 2;
119         static final int VERSION_ADD_NETWORK_ID = 3;
120         static final int VERSION_ADD_METERED = 4;
121         static final int VERSION_ADD_DEFAULT_NETWORK = 5;
122         static final int VERSION_ADD_OEM_MANAGED_NETWORK = 6;
123         static final int VERSION_ADD_SUB_ID = 7;
124     }
125 
126     /**
127      * File header magic number: "ANET". The definition is copied from NetworkStatsCollection,
128      * but it is fine for OEM to re-define to their own value to adapt the legacy file reading
129      * logic.
130      */
131     private static final int FILE_MAGIC = 0x414E4554;
132     /** Default buffer size from BufferedInputStream */
133     private static final int BUFFER_SIZE = 8192;
134 
135     // Constructing this object is not allowed.
NetworkStatsDataMigrationUtils()136     private NetworkStatsDataMigrationUtils() {
137     }
138 
139     // Used to read files at /data/system/netstats_*.bin.
140     @NonNull
getPlatformSystemDir()141     private static File getPlatformSystemDir() {
142         return new File(Environment.getDataDirectory(), "system");
143     }
144 
145     // Used to read files at /data/system/netstats/<tag>.<start>-<end>.
146     @NonNull
getPlatformBaseDir()147     private static File getPlatformBaseDir() {
148         File baseDir = new File(getPlatformSystemDir(), "netstats");
149         baseDir.mkdirs();
150         return baseDir;
151     }
152 
153     // Get /data/system/netstats_*.bin legacy files. Does not check for existence.
154     @NonNull
getLegacyBinFileForPrefix(@onNull @refix String prefix)155     private static File getLegacyBinFileForPrefix(@NonNull @Prefix String prefix) {
156         return new File(getPlatformSystemDir(), sPrefixLegacyFileNameMap.get(prefix));
157     }
158 
159     // List /data/system/netstats/[xt|uid|uid_tag].<start>-<end> legacy files.
160     @NonNull
getPlatformFileListForPrefix(@onNull @refix String prefix)161     private static ArrayList<File> getPlatformFileListForPrefix(@NonNull @Prefix String prefix) {
162         final ArrayList<File> list = new ArrayList<>();
163         final File platformFiles = getPlatformBaseDir();
164         if (platformFiles.exists()) {
165             final String[] files = platformFiles.list();
166             if (files == null) return list;
167             Arrays.sort(files);
168             for (String name : files) {
169                 // Skip when prefix doesn't match.
170                 if (!name.startsWith(prefix + ".")) continue;
171 
172                 list.add(new File(platformFiles, name));
173             }
174         }
175         return list;
176     }
177 
178     /**
179      * Read legacy persisted network stats from disk.
180      *
181      * This function provides the implementation to read legacy network stats
182      * from disk. It is used for migration of legacy network stats into the
183      * stats provided by the Connectivity module.
184      * This function needs to know about the previous format(s) of the network
185      * stats data that might be stored on this device so it can be read and
186      * conserved upon upgrade to Android 13 or above.
187      *
188      * This function will be called multiple times sequentially, all on the
189      * same thread, and will not be called multiple times concurrently. This
190      * function is expected to do a substantial amount of disk access, and
191      * doesn't need to return particularly fast, but the first boot after
192      * an upgrade to Android 13+ will be held until migration is done. As
193      * migration is only necessary once, after the first boot following the
194      * upgrade, this delay is not incurred.
195      *
196      * If this function fails in any way, it should throw an exception. If this
197      * happens, the system can't know about the data that was stored in the
198      * legacy files, but it will still count data usage happening on this
199      * session. On the next boot, the system will try migration again, and
200      * merge the returned data with the data used with the previous session.
201      * The system will only try the migration up to three (3) times. The remaining
202      * count is stored in the netstats_import_legacy_file_needed device config. The
203      * legacy data is never deleted by the mainline module to avoid any possible
204      * data loss.
205      *
206      * It is possible to set the netstats_import_legacy_file_needed device config
207      * to any positive integer to force the module to perform the migration. This
208      * can be achieved by calling the following command before rebooting :
209      *     adb shell device_config put connectivity netstats_import_legacy_file_needed 1
210      *
211      * The AOSP implementation provides code to read persisted network stats as
212      * they were written by AOSP prior to Android 13.
213      * OEMs who have used the AOSP implementation of persisting network stats
214      * to disk don't need to change anything.
215      * OEM that had modifications to this format should modify this function
216      * to read from their custom file format or locations if necessary.
217      *
218      * @param prefix         Type of data which is being read by the service.
219      * @param bucketDuration Duration of the buckets of the object, in milliseconds.
220      * @return {@link NetworkStatsCollection} instance.
221      */
222     @NonNull
readPlatformCollection( @onNull @refix String prefix, long bucketDuration)223     public static NetworkStatsCollection readPlatformCollection(
224             @NonNull @Prefix String prefix, long bucketDuration) throws IOException {
225         final NetworkStatsCollection.Builder builder =
226                 new NetworkStatsCollection.Builder(bucketDuration);
227 
228         // Import /data/system/netstats_uid.bin legacy files if exists.
229         switch (prefix) {
230             case PREFIX_UID:
231             case PREFIX_UID_TAG:
232                 final File uidFile = getLegacyBinFileForPrefix(prefix);
233                 if (uidFile.exists()) {
234                     readLegacyUid(builder, uidFile, PREFIX_UID_TAG.equals(prefix) ? true : false);
235                 }
236                 break;
237             default:
238                 // Ignore other types.
239         }
240 
241         // Import /data/system/netstats/[xt|uid|uid_tag].<start>-<end> legacy files if exists.
242         final ArrayList<File> platformFiles = getPlatformFileListForPrefix(prefix);
243         for (final File platformFile : platformFiles) {
244             if (platformFile.exists()) {
245                 readPlatformCollection(builder, platformFile);
246             }
247         }
248 
249         return builder.build();
250     }
251 
readPlatformCollection(@onNull NetworkStatsCollection.Builder builder, @NonNull File file)252     private static void readPlatformCollection(@NonNull NetworkStatsCollection.Builder builder,
253             @NonNull File file) throws IOException {
254         final FileInputStream is = new FileInputStream(file);
255         final ArtFastDataInput dataIn = new ArtFastDataInput(is, BUFFER_SIZE);
256         try {
257             readPlatformCollection(builder, dataIn);
258         } finally {
259             IoUtils.closeQuietly(dataIn);
260         }
261     }
262 
263     /**
264      * Helper function to read old version of NetworkStatsCollections that resided in the platform.
265      *
266      * @hide
267      */
268     @VisibleForTesting
readPlatformCollection(@onNull NetworkStatsCollection.Builder builder, @NonNull DataInput in)269     public static void readPlatformCollection(@NonNull NetworkStatsCollection.Builder builder,
270             @NonNull DataInput in) throws IOException {
271         // verify file magic header intact
272         final int magic = in.readInt();
273         if (magic != FILE_MAGIC) {
274             throw new ProtocolException("unexpected magic: " + magic);
275         }
276 
277         final int version = in.readInt();
278         switch (version) {
279             case CollectionVersion.VERSION_UNIFIED_INIT: {
280                 // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
281                 final int identSize = in.readInt();
282                 for (int i = 0; i < identSize; i++) {
283                     final Set<NetworkIdentity> ident = readPlatformNetworkIdentitySet(in);
284 
285                     final int size = in.readInt();
286                     for (int j = 0; j < size; j++) {
287                         final int uid = in.readInt();
288                         final int set = in.readInt();
289                         final int tag = in.readInt();
290 
291                         final NetworkStatsCollection.Key key = new NetworkStatsCollection.Key(
292                                 ident, uid, set, tag);
293                         final NetworkStatsHistory history = readPlatformHistory(in);
294                         builder.addEntry(key, history);
295                     }
296                 }
297                 break;
298             }
299             default: {
300                 throw new ProtocolException("unexpected version: " + version);
301             }
302         }
303     }
304 
305     // Copied from NetworkStatsHistory#DataStreamUtils.
readFullLongArray(DataInput in)306     private static long[] readFullLongArray(DataInput in) throws IOException {
307         final int size = in.readInt();
308         if (size < 0) throw new ProtocolException("negative array size");
309         final long[] values = new long[size];
310         for (int i = 0; i < values.length; i++) {
311             values[i] = in.readLong();
312         }
313         return values;
314     }
315 
316     // Copied from NetworkStatsHistory#DataStreamUtils.
readVarLongArray(@onNull DataInput in)317     private static long[] readVarLongArray(@NonNull DataInput in) throws IOException {
318         final int size = in.readInt();
319         if (size == -1) return null;
320         if (size < 0) throw new ProtocolException("negative array size");
321         final long[] values = new long[size];
322         for (int i = 0; i < values.length; i++) {
323             values[i] = readVarLong(in);
324         }
325         return values;
326     }
327 
328     /**
329      * Read variable-length {@link Long} using protobuf-style approach.
330      */
331     // Copied from NetworkStatsHistory#DataStreamUtils.
readVarLong(DataInput in)332     private static long readVarLong(DataInput in) throws IOException {
333         int shift = 0;
334         long result = 0;
335         while (shift < 64) {
336             byte b = in.readByte();
337             result |= (long) (b & 0x7F) << shift;
338             if ((b & 0x80) == 0) {
339                 return result;
340             }
341             shift += 7;
342         }
343         throw new ProtocolException("malformed var long");
344     }
345 
346     // Copied from NetworkIdentitySet.
readOptionalString(DataInput in)347     private static String readOptionalString(DataInput in) throws IOException {
348         if (in.readByte() != 0) {
349             return in.readUTF();
350         } else {
351             return null;
352         }
353     }
354 
355     /**
356      * This is copied from NetworkStatsHistory#NetworkStatsHistory(DataInput in). But it is fine
357      * for OEM to re-write the logic to adapt the legacy file reading.
358      */
359     @NonNull
readPlatformHistory(@onNull DataInput in)360     private static NetworkStatsHistory readPlatformHistory(@NonNull DataInput in)
361             throws IOException {
362         final long bucketDuration;
363         final long[] bucketStart;
364         final long[] rxBytes;
365         final long[] rxPackets;
366         final long[] txBytes;
367         final long[] txPackets;
368         final long[] operations;
369         final int bucketCount;
370         long[] activeTime = new long[0];
371 
372         final int version = in.readInt();
373         switch (version) {
374             case HistoryVersion.VERSION_INIT: {
375                 bucketDuration = in.readLong();
376                 bucketStart = readFullLongArray(in);
377                 rxBytes = readFullLongArray(in);
378                 rxPackets = new long[bucketStart.length];
379                 txBytes = readFullLongArray(in);
380                 txPackets = new long[bucketStart.length];
381                 operations = new long[bucketStart.length];
382                 bucketCount = bucketStart.length;
383                 break;
384             }
385             case HistoryVersion.VERSION_ADD_PACKETS:
386             case HistoryVersion.VERSION_ADD_ACTIVE: {
387                 bucketDuration = in.readLong();
388                 bucketStart = readVarLongArray(in);
389                 activeTime = (version >= HistoryVersion.VERSION_ADD_ACTIVE)
390                         ? readVarLongArray(in)
391                         : new long[bucketStart.length];
392                 rxBytes = readVarLongArray(in);
393                 rxPackets = readVarLongArray(in);
394                 txBytes = readVarLongArray(in);
395                 txPackets = readVarLongArray(in);
396                 operations = readVarLongArray(in);
397                 bucketCount = bucketStart.length;
398                 break;
399             }
400             default: {
401                 throw new ProtocolException("unexpected version: " + version);
402             }
403         }
404 
405         final NetworkStatsHistory.Builder historyBuilder =
406                 new NetworkStatsHistory.Builder(bucketDuration, bucketCount);
407         for (int i = 0; i < bucketCount; i++) {
408             final NetworkStatsHistory.Entry entry = new NetworkStatsHistory.Entry(
409                     bucketStart[i], activeTime[i],
410                     rxBytes[i], rxPackets[i], txBytes[i], txPackets[i], operations[i]);
411             historyBuilder.addEntry(entry);
412         }
413 
414         return historyBuilder.build();
415     }
416 
417     @NonNull
readPlatformNetworkIdentitySet(@onNull DataInput in)418     private static Set<NetworkIdentity> readPlatformNetworkIdentitySet(@NonNull DataInput in)
419             throws IOException {
420         final int version = in.readInt();
421         final int size = in.readInt();
422         final Set<NetworkIdentity> set = new HashSet<>();
423         for (int i = 0; i < size; i++) {
424             if (version <= IdentitySetVersion.VERSION_INIT) {
425                 final int ignored = in.readInt();
426             }
427             final int type = in.readInt();
428             final int ratType = in.readInt();
429             final String subscriberId = readOptionalString(in);
430             final String networkId;
431             if (version >= IdentitySetVersion.VERSION_ADD_NETWORK_ID) {
432                 networkId = readOptionalString(in);
433             } else {
434                 networkId = null;
435             }
436             final boolean roaming;
437             if (version >= IdentitySetVersion.VERSION_ADD_ROAMING) {
438                 roaming = in.readBoolean();
439             } else {
440                 roaming = false;
441             }
442 
443             final boolean metered;
444             if (version >= IdentitySetVersion.VERSION_ADD_METERED) {
445                 metered = in.readBoolean();
446             } else {
447                 // If this is the old data and the type is mobile, treat it as metered. (Note that
448                 // if this is a mobile network, TYPE_MOBILE is the only possible type that could be
449                 // used.)
450                 metered = (type == TYPE_MOBILE);
451             }
452 
453             final boolean defaultNetwork;
454             if (version >= IdentitySetVersion.VERSION_ADD_DEFAULT_NETWORK) {
455                 defaultNetwork = in.readBoolean();
456             } else {
457                 defaultNetwork = true;
458             }
459 
460             final int oemNetCapabilities;
461             if (version >= IdentitySetVersion.VERSION_ADD_OEM_MANAGED_NETWORK) {
462                 oemNetCapabilities = in.readInt();
463             } else {
464                 oemNetCapabilities = NetworkTemplate.OEM_MANAGED_NO;
465             }
466 
467             final int subId;
468             if (version >= IdentitySetVersion.VERSION_ADD_SUB_ID) {
469                 subId = in.readInt();
470             } else {
471                 subId = INVALID_SUBSCRIPTION_ID;
472             }
473 
474             // Legacy files might contain TYPE_MOBILE_* types which were deprecated in later
475             // releases. For backward compatibility, record them as TYPE_MOBILE instead.
476             final int collapsedLegacyType = getCollapsedLegacyType(type);
477             final NetworkIdentity.Builder builder = new NetworkIdentity.Builder()
478                     .setType(collapsedLegacyType)
479                     .setSubscriberId(subscriberId)
480                     .setWifiNetworkKey(networkId)
481                     .setRoaming(roaming).setMetered(metered)
482                     .setDefaultNetwork(defaultNetwork)
483                     .setOemManaged(oemNetCapabilities)
484                     .setSubId(subId);
485             if (type == TYPE_MOBILE && ratType != NetworkTemplate.NETWORK_TYPE_ALL) {
486                 builder.setRatType(ratType);
487             }
488             set.add(builder.build());
489         }
490         return set;
491     }
492 
getCollapsedLegacyType(int networkType)493     private static int getCollapsedLegacyType(int networkType) {
494         // The constants are referenced from ConnectivityManager#TYPE_MOBILE_*.
495         switch (networkType) {
496             case TYPE_MOBILE:
497             case TYPE_MOBILE_SUPL:
498             case TYPE_MOBILE_MMS:
499             case TYPE_MOBILE_DUN:
500             case TYPE_MOBILE_HIPRI:
501             case 10 /* TYPE_MOBILE_FOTA */:
502             case 11 /* TYPE_MOBILE_IMS */:
503             case 12 /* TYPE_MOBILE_CBS */:
504             case 14 /* TYPE_MOBILE_IA */:
505             case 15 /* TYPE_MOBILE_EMERGENCY */:
506                 return TYPE_MOBILE;
507         }
508         return networkType;
509     }
510 
readLegacyUid(@onNull NetworkStatsCollection.Builder builder, @NonNull File uidFile, boolean onlyTaggedData)511     private static void readLegacyUid(@NonNull NetworkStatsCollection.Builder builder,
512             @NonNull File uidFile, boolean onlyTaggedData) throws IOException {
513         final AtomicFile inputFile = new AtomicFile(uidFile);
514         DataInputStream in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
515         try {
516             readLegacyUid(builder, in, onlyTaggedData);
517         } finally {
518             IoUtils.closeQuietly(in);
519         }
520     }
521 
522     /**
523      * Read legacy Uid statistics file format into the collection.
524      *
525      * This is copied from {@code NetworkStatsCollection#readLegacyUid}.
526      * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
527      *
528      * @param taggedData whether to read only tagged data (true) or only non-tagged data
529      *                   (false). For legacy uid files, the tagged data was stored in
530      *                   the same binary file with non-tagged data. But in later releases,
531      *                   these data should be kept in different recorders.
532      * @hide
533      */
534     @VisibleForTesting
readLegacyUid(@onNull NetworkStatsCollection.Builder builder, @NonNull DataInput in, boolean taggedData)535     public static void readLegacyUid(@NonNull NetworkStatsCollection.Builder builder,
536             @NonNull DataInput in, boolean taggedData) throws IOException {
537         try {
538             // verify file magic header intact
539             final int magic = in.readInt();
540             if (magic != FILE_MAGIC) {
541                 throw new ProtocolException("unexpected magic: " + magic);
542             }
543 
544             final int version = in.readInt();
545             switch (version) {
546                 case CollectionVersion.VERSION_UID_INIT: {
547                     // uid := size *(UID NetworkStatsHistory)
548                     // drop this data version, since we don't have a good
549                     // mapping into NetworkIdentitySet.
550                     break;
551                 }
552                 case CollectionVersion.VERSION_UID_WITH_IDENT: {
553                     // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
554                     // drop this data version, since this version only existed
555                     // for a short time.
556                     break;
557                 }
558                 case CollectionVersion.VERSION_UID_WITH_TAG:
559                 case CollectionVersion.VERSION_UID_WITH_SET: {
560                     // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
561                     final int identSize = in.readInt();
562                     for (int i = 0; i < identSize; i++) {
563                         final Set<NetworkIdentity> ident = readPlatformNetworkIdentitySet(in);
564 
565                         final int size = in.readInt();
566                         for (int j = 0; j < size; j++) {
567                             final int uid = in.readInt();
568                             final int set = (version >= CollectionVersion.VERSION_UID_WITH_SET)
569                                     ? in.readInt()
570                                     : SET_DEFAULT;
571                             final int tag = in.readInt();
572 
573                             final NetworkStatsCollection.Key key = new NetworkStatsCollection.Key(
574                                     ident, uid, set, tag);
575                             final NetworkStatsHistory history = readPlatformHistory(in);
576 
577                             if ((tag == TAG_NONE) != taggedData) {
578                                 builder.addEntry(key, history);
579                             }
580                         }
581                     }
582                     break;
583                 }
584                 default: {
585                     throw new ProtocolException("unknown version: " + version);
586                 }
587             }
588         } catch (FileNotFoundException | ProtocolException e) {
589             // missing stats is okay, probably first boot
590         }
591     }
592 }
593