// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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 checkinparse contains functions to parse checkin report into batterystats proto.
package checkinparse

import (
	"errors"
	"fmt"
	"math"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/golang/protobuf/proto"

	"github.com/google/battery-historian/build"
	"github.com/google/battery-historian/checkinutil"
	"github.com/google/battery-historian/historianutils"
	"github.com/google/battery-historian/packageutils"
	"github.com/google/battery-historian/sliceparse"

	bspb "github.com/google/battery-historian/pb/batterystats_proto"
	sessionpb "github.com/google/battery-historian/pb/session_proto"
	usagepb "github.com/google/battery-historian/pb/usagestats_proto"
)

const (
	// minimum number of fields any type of battery stats have
	minNumFields = 4
	// Current range of supported/expected checkin versions.
	minParseReportVersion = 11
	maxParseReportVersion = 17
)

// Possible battery stats categories generated by on device java code.
const (
	info         = "i"
	sinceCharged = "l" // SINCE_CHARGED: Since last charged: The only reliable value.
	current      = "c" // Deprecated
	unplugged    = "u" // Deprecated: SINCE_UNPLUGGED: Very unreliable and soon to be removed.
)

// String representations of categories the parsing code handles. Contains all
// categories defined in frameworks/base/core/java/android/os/BatteryStats.java
// unless explicitly stated.
const (
	versionData                   = "vers"
	uidData                       = "uid"
	apkData                       = "apk"
	processData                   = "pr"
	cpuData                       = "cpu"
	sensorData                    = "sr"
	vibratorData                  = "vib"
	foregroundData                = "fg"
	stateTimeData                 = "st"
	wakelockData                  = "wl"
	syncData                      = "sy"
	jobData                       = "jb"
	kernelWakelockData            = "kwl"
	wakeupReasonData              = "wr"
	networkData                   = "nt"
	userActivityData              = "ua"
	batteryData                   = "bt"
	batteryDischargeData          = "dc"
	batteryLevelData              = "lv"
	globalWifiData                = "gwfl"
	globalWifiControllerData      = "gwfcd"
	wifiControllerData            = "wfcd"
	wifiData                      = "wfl"
	bluetoothControllerData       = "ble"
	globalBluetoothControllerData = "gble" // Previously globalBluetoothData
	miscData                      = "m"
	modemControllerData           = "mcd"
	globalModemControllerData     = "gmcd"
	globalNetworkData             = "gn"
	// HISTORY_STRING_POOL (hsp) is not included in the checkin log.
	// HISTORY_DATA (h) is not included in the checkin log.
	screenBrightnessData        = "br"
	signalStrengthTimeData      = "sgt"
	signalScanningTimeData      = "sst"
	signalStrengthCountData     = "sgc"
	dataConnectionTimeData      = "dct"
	dataConnectionCountData     = "dcc"
	wifiStateTimeData           = "wst"
	wifiStateCountData          = "wsc"
	wifiSupplStateTimeData      = "wsst"
	wifiSupplStateCountData     = "wssc"
	wifiSignalStrengthTimeData  = "wsgt"
	wifiSignalStrengthCountData = "wsgc"
	bluetoothStateTimeData      = "bst"
	bluetoothStateCountData     = "bsc"
	powerUseSummaryData         = "pws"
	powerUseItemData            = "pwi"
	dischargeStepData           = "dsd"
	chargeStepData              = "csd"
	dischargeTimeRemainData     = "dtr"
	chargeTimeRemainData        = "ctr"
	flashlightData              = "fla"
	cameraData                  = "cam"
	videoData                   = "vid"
	audioData                   = "aud"
)

var (
	powerUseItemNameMap = map[string]bspb.BatteryStats_System_PowerUseItem_Name{
		"idle":       bspb.BatteryStats_System_PowerUseItem_IDLE,
		"cell":       bspb.BatteryStats_System_PowerUseItem_CELL,
		"phone":      bspb.BatteryStats_System_PowerUseItem_PHONE,
		"wifi":       bspb.BatteryStats_System_PowerUseItem_WIFI,
		"blue":       bspb.BatteryStats_System_PowerUseItem_BLUETOOTH,
		"scrn":       bspb.BatteryStats_System_PowerUseItem_SCREEN,
		"uid":        bspb.BatteryStats_System_PowerUseItem_APP,
		"user":       bspb.BatteryStats_System_PowerUseItem_USER,
		"unacc":      bspb.BatteryStats_System_PowerUseItem_UNACCOUNTED,
		"over":       bspb.BatteryStats_System_PowerUseItem_OVERCOUNTED,
		"???":        bspb.BatteryStats_System_PowerUseItem_DEFAULT,
		"flashlight": bspb.BatteryStats_System_PowerUseItem_FLASHLIGHT,
	}

	// sharedUIDLabelMap contains a mapping of known shared UID labels to predefined group names.
	sharedUIDLabelMap = map[string]string{
		"android.media":                          "MEDIA",
		"android.uid.bluetooth":                  "BLUETOOTH",
		"android.uid.nfc":                        "NFC",
		"android.uid.phone":                      "RADIO",             // Associated with UID 1001
		"android.uid.shared":                     "CONTACTS_PROVIDER", // "com.android.providers.contacts" is a prominent member of the UID group
		"android.uid.shell":                      "SHELL",
		"android.uid.system":                     "ANDROID_SYSTEM",
		"android.uid.systemui":                   "SYSTEM_UI",
		"com.google.android.calendar.uid.shared": "GOOGLE_CALENDAR",
		"com.google.uid.shared":                  "GOOGLE_SERVICES",
	}

	// TODO: get rid of packageNameToSharedUIDMap
	// Contains a mapping of known packages with their shared UIDs.
	packageNameToSharedUIDMap = map[string]string{
		// com.google.uid.shared
		"com.google.android.apps.gtalkservice":    "GOOGLE_SERVICES",
		"com.google.android.backuptransport":      "GOOGLE_SERVICES",
		"com.google.android.gms":                  "GOOGLE_SERVICES",
		"com.google.android.gms.car.userfeedback": "GOOGLE_SERVICES",
		"com.google.android.googleapps":           "GOOGLE_SERVICES",
		"com.google.android.gsf":                  "GOOGLE_SERVICES",
		"com.google.android.gsf.login":            "GOOGLE_SERVICES",
		"com.google.android.gsf.notouch":          "GOOGLE_SERVICES",
		"com.google.android.providers.gmail":      "GOOGLE_SERVICES",
		"com.google.android.sss.authbridge":       "GOOGLE_SERVICES",
		"com.google.gch.gateway":                  "GOOGLE_SERVICES",

		// com.google.android.calendar.uid.shared
		"com.android.calendar":                     "GOOGLE_CALENDAR",
		"com.google.android.calendar":              "GOOGLE_CALENDAR",
		"com.google.android.syncadapters.calendar": "GOOGLE_CALENDAR",

		// android.uid.system
		"android":                            "ANDROID_SYSTEM",
		"com.android.changesettings":         "ANDROID_SYSTEM",
		"com.android.inputdevices":           "ANDROID_SYSTEM",
		"com.android.keychain":               "ANDROID_SYSTEM",
		"com.android.location.fused":         "ANDROID_SYSTEM",
		"com.android.providers.settings":     "ANDROID_SYSTEM",
		"com.android.settings":               "ANDROID_SYSTEM",
		"com.google.android.canvas.settings": "ANDROID_SYSTEM",
		"com.lge.SprintHiddenMenu":           "ANDROID_SYSTEM",
		"com.nvidia.tegraprofiler.security":  "ANDROID_SYSTEM",
		"com.qualcomm.atfwd":                 "ANDROID_SYSTEM",
		"com.qualcomm.display":               "ANDROID_SYSTEM",

		// android.uid.phone, associated with UID 1001
		"com.android.phone":                    "RADIO",
		"com.android.providers.telephony":      "RADIO",
		"com.android.sdm.plugins.connmo":       "RADIO",
		"com.android.sdm.plugins.dcmo":         "RADIO",
		"com.android.sdm.plugins.sprintdm":     "RADIO",
		"com.htc.android.qxdm2sd":              "RADIO",
		"com.android.mms.service":              "RADIO",
		"com.android.server.telecom":           "RADIO",
		"com.android.sprint.lifetimedata":      "RADIO",
		"com.android.stk":                      "RADIO",
		"com.motorola.service.ims":             "RADIO",
		"com.qualcomm.qti.imstestrunner":       "RADIO",
		"com.qualcomm.qti.rcsbootstraputil":    "RADIO",
		"com.qualcomm.qti.rcsimsbootstraputil": "RADIO",
		"com.qualcomm.qcrilmsgtunnel":          "RADIO",
		"com.qualcomm.shutdownlistner":         "RADIO",
		"org.codeaurora.ims":                   "RADIO",
		"com.asus.atcmd":                       "RADIO",
		"com.mediatek.imeiwriter":              "RADIO",

		// android.uid.nfc
		"com.android.nfc":                 "NFC",
		"com.android.nfc3":                "NFC",
		"com.google.android.uiccwatchdog": "NFC",

		// android.uid.shared
		"com.android.contacts":                 "CONTACTS_PROVIDER",
		"com.android.providers.applications":   "CONTACTS_PROVIDER",
		"com.android.providers.contacts":       "CONTACTS_PROVIDER",
		"com.android.providers.userdictionary": "CONTACTS_PROVIDER",

		// android.media
		"com.android.gallery":                "MEDIA",
		"com.android.providers.downloads":    "MEDIA",
		"com.android.providers.downloads.ui": "MEDIA",
		"com.android.providers.drm":          "MEDIA",
		"com.android.providers.media":        "MEDIA",

		// android.uid.systemui
		"com.android.keyguard": "SYSTEM_UI",
		"com.android.systemui": "SYSTEM_UI",

		// android.uid.bluetooth
		"com.android.bluetooth": "BLUETOOTH",

		// android.uid.shell
		"com.android.shell": "SHELL",
	}

	// KnownUIDs lists all constant UIDs defined in system/core/include/private/android_filesystem_config.h.
	// GIDs are excluded. These should never be renumbered.
	KnownUIDs = map[int32]string{
		0:    "ROOT",           // traditional unix root user
		1000: "ANDROID_SYSTEM", // system server
		1001: "RADIO",          // telephony subsystem, RIL
		1002: "BLUETOOTH",      // bluetooth subsystem
		1003: "GRAPHICS",       // graphics devices
		1004: "INPUT",          // input devices
		1005: "AUDIO",          // audio devices
		1006: "CAMERA",         // camera devices
		1007: "LOG",            // log devices
		1008: "COMPASS",        // compass device
		1009: "MOUNT",          // mountd socket
		1010: "WIFI",           // wifi subsystem
		1011: "ADB",            // android debug bridge (adbd)
		1012: "INSTALL",        // group for installing packages
		1013: "MEDIA",          // mediaserver process
		1014: "DHCP",           // dhcp client
		1015: "SDCARD_RW",      // external storage write access
		1016: "VPN",            // vpn system
		1017: "KEYSTORE",       // keystore subsystem
		1018: "USB",            // USB devices
		1019: "DRM",            // DRM server
		1020: "MDNSR",          // MulticastDNSResponder (service discovery)
		1021: "GPS",            // GPS daemon
		// 1022 is deprecated and unused
		1023: "MEDIA_RW", // internal media storage write access
		1024: "MTP",      // MTP USB driver access
		// 1025 is deprecated and unused
		1026: "DRMRPC",       // group for drm rpc
		1027: "NFC",          // nfc subsystem
		1028: "SDCARD_R",     // external storage read access
		1029: "CLAT",         // clat part of nat464
		1030: "LOOP_RADIO",   // loop radio devices
		1031: "MEDIA_DRM",    // MediaDrm plugins
		1032: "PACKAGE_INFO", // access to installed package details
		1033: "SDCARD_PICS",  // external storage photos access
		1034: "SDCARD_AV",    // external storage audio/video access
		1035: "SDCARD_ALL",   // access all users external storage
		1036: "LOGD",         // log daemon
		1037: "SHARED_RELRO", // creator of shared GNU RELRO files
		1038: "DBUS",         // dbus-daemon IPC broker process
		1039: "TLSDATE",      // tlsdate unprivileged user
		1040: "MEDIA_EX",     // mediaextractor process
		1041: "AUDIOSERVER",  // audioserver process
		1042: "METRICS_COLL", // metrics_collector process
		1043: "METRICSD",     // metricsd process
		1044: "WEBSERV",      // webservd process
		1045: "DEBUGGERD",    // debuggerd unprivileged user
		1046: "MEDIA_CODEC",  // mediacodec process
		1047: "CAMERASERVER", // cameraserver process
		1048: "FIREWALL",     // firewalld process
		1049: "TRUNKS",       // trunksd process (TPM daemon)

		2000: "SHELL", // adb and debug shell user
		2001: "CACHE", // cache access
		2002: "DIAG",  // access to diagnostic resources

		// 2900-2999 is reserved for OEM

		// The 3000 series are intended for use as supplemental group id's only.
		// They indicate special Android capabilities that the kernel is aware of.
		3001: "NET_BT_ADMIN", // bluetooth: create any socket
		3002: "NET_BT",       // bluetooth: create sco, rfcomm or l2cap sockets
		3003: "INET",         // can create AF_INET and AF_INET6 sockets
		3004: "NET_RAW",      // can create raw INET sockets
		3005: "NET_ADMIN",    // can configure interfaces and routing tables
		3006: "NET_BW_STATS", // read bandwidth statistics
		3007: "NET_BW_ACCT",  // change bandwidth statistics accounting
		3008: "NET_BT_STACK", // bluetooth: access config files
		3009: "READPROC",     // allow /proc read access
		3010: "WAKELOCK",     // allow system wakelock read/write access

		// 5000-5999 is reserved for OEM

		9997: "EVERYBODY", // shared between all apps in the same profile
		9998: "MISC",      // access to misc storage
		9999: "NOBODY",
	}
)

// PackageUIDGroupName returns the predefined group name for the given package name.
// It will return an empty string if there is no predefined group name.
func PackageUIDGroupName(pkg string) string {
	return packageNameToSharedUIDMap[pkg]
}

type stepData struct {
	timeMsec      float32
	level         float32
	displayState  *bspb.BatteryStats_System_DisplayState_State
	powerSaveMode *bspb.BatteryStats_System_PowerSaveMode_Mode
	idleMode      *bspb.BatteryStats_System_IdleMode_Mode
}

// WakelockInfo is a data structure used to sort wakelocks by time or count.
type WakelockInfo struct {
	Name     string
	UID      int32
	Duration time.Duration
	Count    float32
}

// byAbsTime sorts wakelock by absolute value of the time held.
type byAbsTime []*WakelockInfo

func (a byAbsTime) Len() int      { return len(a) }
func (a byAbsTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// sort by decreasing absolute value of time
func (a byAbsTime) Less(i, j int) bool {
	x, y := a[i].Duration, a[j].Duration
	return math.Abs(float64(x)) >= math.Abs(float64(y))
}

// SortByAbsTime ranks a slice of wakelocks by the absolute value of duration in desc order.
func SortByAbsTime(items []*WakelockInfo) {
	sort.Sort(byAbsTime(items))
}

// byTime sorts wakelock by the time held.
type byTime []*WakelockInfo

func (a byTime) Len() int      { return len(a) }
func (a byTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// sort by decreasing time order then increasing alphabetic order to break the tie.
func (a byTime) Less(i, j int) bool {
	if x, y := a[i].Duration, a[j].Duration; x != y {
		return x > y
	}
	return a[i].Name < a[j].Name
}

// byCount sorts wakelock by count.
type byCount []*WakelockInfo

func (a byCount) Len() int      { return len(a) }
func (a byCount) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// sort by decreasing time order then increasing alphabetic order to break the tie
func (a byCount) Less(i, j int) bool {
	if x, y := a[i].Count, a[j].Count; x != y {
		return x > y
	}
	return a[i].Name < a[j].Name
}

// SortByTime ranks a slice of wakelocks by duration.
func SortByTime(items []*WakelockInfo) {
	sort.Sort(byTime(items))
}

// SortByCount ranks a slice of wakelocks by count.
func SortByCount(items []*WakelockInfo) {
	sort.Sort(byCount(items))
}

// ParseBatteryStats parses the aggregated battery stats in checkin report
// according to frameworks/base/core/java/android/os/BatteryStats.java.
func ParseBatteryStats(pc checkinutil.Counter, cr *checkinutil.CheckinReport, pkgs []*usagepb.PackageInfo) (*bspb.BatteryStats, []string, []error) {
	// Support a single version and single aggregation type in a checkin report.
	var aggregationType bspb.BatteryStats_AggregationType
	var allAppComputedPowerMah float32
	apkSeen := make(map[apkID]bool)

	p := &bspb.BatteryStats{}
	p.System = &bspb.BatteryStats_System{}
	reportVersion := int32(-1) // Default to -1 so we can tell if the report version was parsed from the report.
	var warnings []string
	uids, errs := parsePackageManager(pc, cr.RawBatteryStats, pkgs)
	if len(errs) > 0 {
		return nil, warnings, errs
	}
	for _, r := range cr.RawBatteryStats {
		var rawUID int32
		var rawAggregationType, section string
		// The first element in r is '9', which used to be the report version but is now just there as a legacy field.
		remaining, err := parseSlice(pc, "All", r[1:], &rawUID, &rawAggregationType, &section)
		if err != nil {
			errs = append(errs, fmt.Errorf("error parsing entire line: %v", err))
			continue
		}

		if section == uidData {
			// uidData is parsed in the PackageManager function
			continue
		}

		if section == versionData { // first line may contain version information in new format
			if reportVersion != -1 {
				errs = append(errs, fmt.Errorf("multiple %s lines encountered", versionData))
			}
			// e.g., 9,0,i,vers,15,135,MMB08I,MMB08K
			if _, err := parseSlice(pc, versionData, remaining, &reportVersion); err != nil {
				errs = append(errs, fmt.Errorf("error parsing version data: %v", err))
			}
			continue
		}

		switch rawAggregationType {
		case sinceCharged:
			aggregationType = bspb.BatteryStats_SINCE_CHARGED
		case current:
			aggregationType = bspb.BatteryStats_CURRENT
		case unplugged:
			aggregationType = bspb.BatteryStats_SINCE_UNPLUGGED
		case info: // Not an aggregation type so nothing to do.
		default:
			errs = append(errs, fmt.Errorf("unsupported aggregation type %s", rawAggregationType))
			return nil, warnings, errs
		}

		uid := packageutils.AppID(rawUID)
		if uid == 0 {
			// Some lines that are parsed will provide a uid of 0, even though 0 is not the
			// correct uid for the relevant package.
			// See if we can find an application to claim this.
			pkg, err := packageutils.GuessPackage(strings.Join(remaining, ","), "", pkgs)
			if err != nil {
				errs = append(errs, err)
			}
			// Many applications will match with android, but we should already have the android
			// UID in the map, so ignore if a package matches with android.
			if pkg.GetPkgName() != "" && pkg.GetPkgName() != "android" {
				uid = packageutils.AppID(pkg.GetUid())
			}
		}
		stats, ok := uids[uid]
		if !ok {
			// Unexpected UID. Some packages are uploaded with a uid of 0 and so won't be found
			// until we come across a case like this. Try to determine package from list.
			// Passing an empty uid here because if we reach this case, then the uploaded package
			// in the list has a uid of 0.
			pkg, err := packageutils.GuessPackage(strings.Join(remaining, ","), "", pkgs)
			if err != nil {
				errs = append(errs, err)
			}
			if pkg.GetPkgName() != "" && pkg.GetPkgName() != "android" {
				stats = &bspb.BatteryStats_App{
					Name: pkg.PkgName,
					Uid:  proto.Int32(uid),
					Child: []*bspb.BatteryStats_App_Child{
						{
							Name:        pkg.PkgName,
							VersionCode: pkg.VersionCode,
							VersionName: pkg.VersionName,
						},
					},
				}
				uids[uid] = stats
			} else {
				if _, ok := KnownUIDs[uid]; !ok {
					// We've already gone through the entire package list, and it's not a UID
					// that we already know about, so log a warning.
					if packageutils.IsSandboxedProcess(uid) {
						warnings = append(warnings, fmt.Sprintf("found sandboxed uid for section %q", section))
					} else {
						warnings = append(warnings, fmt.Sprintf("found unexpected uid %d for section %q", uid, section))
					}
				}
				stats = &bspb.BatteryStats_App{Uid: proto.Int32(uid)}
				uids[uid] = stats
			}
		}
		system := p.GetSystem()
		// Parse csv lines according to
		// frameworks/base/core/java/android/os/BatteryStats.java.
		parsed, warn, csErrs := parseSection(pc, reportVersion, rawUID, section, remaining, stats, system, apkSeen, &allAppComputedPowerMah)
		e := false
		if e, warnings, errs = saveWarningsAndErrors(warnings, warn, errs, csErrs...); e {
			return nil, warnings, errs
		}
		if !parsed {
			warnings = append(warnings, fmt.Sprintf("unknown data category %s", section))
		}
	}
	if reportVersion == -1 {
		errs = append(errs, errors.New("no report version found"))
	} else if reportVersion < minParseReportVersion {
		errs = append(errs, fmt.Errorf("old report version %d", reportVersion))
	} else if reportVersion > maxParseReportVersion {
		warnings = append(warnings, fmt.Sprintf("newer report version %d found", reportVersion))
	}

	suidMap := make(map[int32]string)
	for _, pkg := range pkgs {
		if suid := pkg.GetSharedUserId(); suid != "" {
			if s, ok := suidMap[pkg.GetUid()]; ok && s != suid {
				errs = append(errs, fmt.Errorf("got UID %d with two different shared UID labels: %s and %s", pkg.GetUid(), s, suid))
				continue
			}
			suidMap[pkg.GetUid()] = suid
		}
	}
	processAppInfo(uids, suidMap)

	// Copy uids to p.App.
	for _, app := range uids {
		aggregateAppApk(app)
		p.App = append(p.App, app)
	}
	// Copy to p.System.
	p.System.PowerUseItem = append(p.System.PowerUseItem,
		&bspb.BatteryStats_System_PowerUseItem{
			Name:             bspb.BatteryStats_System_PowerUseItem_APP.Enum(),
			ComputedPowerMah: proto.Float32(allAppComputedPowerMah),
		})
	if m := p.System.GetMisc(); m != nil {
		// Screen off time is calculated by subtracting screen on time from total battery real time.
		diff := p.System.GetBattery().GetBatteryRealtimeMsec() - m.GetScreenOnTimeMsec()
		m.ScreenOffTimeMsec = proto.Float32(diff)
		if diff < 0 {
			errs = append(errs, fmt.Errorf("negative screen off time"))
		}
	}
	p.ReportVersion = proto.Int32(reportVersion)
	p.AggregationType = aggregationType.Enum()
	p.StartTimeUsec = proto.Int64(p.System.GetBattery().GetStartClockTimeMsec() * 1000)
	p.EndTimeUsec = proto.Int64(cr.TimeUsec)
	gmsPkg, err := packageutils.GuessPackage("com.google.android.gms", "", pkgs)
	if err != nil {
		errs = append(errs, err)
	}
	if gmsPkg != nil {
		p.GmsVersion = gmsPkg.VersionCode
	}

	p.Build = build.Build(cr.BuildID)
	if p.GetBuild().GetType() == "user" {
		for _, tag := range p.GetBuild().GetTags() {
			if tag == "release-keys" {
				p.IsUserRelease = proto.Bool(true)
				break
			}
		}
	}
	p.DeviceGroup = cr.DeviceGroup
	p.CheckinRule = cr.CheckinRule
	p.Radio = proto.String(cr.Radio)
	p.SdkVersion = proto.Int32(cr.SDKVersion)
	p.Bootloader = proto.String(cr.Bootloader)
	if cr.CellOperator != "" {
		p.Carrier = proto.String(cr.CellOperator + "/" + cr.CountryCode)
	}
	p.CountryCode = proto.String(cr.CountryCode)
	p.TimeZone = proto.String(cr.TimeZone)

	postProcessBatteryStats(p)

	return p, warnings, errs
}

// CreateCheckinReport creates a checkinutil.CheckinReport from the given
// sessionpb.Checkin.
func CreateCheckinReport(cp *sessionpb.Checkin) *checkinutil.CheckinReport {
	return &checkinutil.CheckinReport{
		TimeUsec:        cp.GetBucketSnapshotMsec() * 1000,
		TimeZone:        cp.GetSystemInfo().GetTimeZone(),
		AndroidID:       cp.GetAndroidId(),
		DeviceGroup:     cp.Groups,
		BuildID:         cp.GetBuildFingerprint(),
		Radio:           cp.GetSystemInfo().GetBasebandRadio(),
		Bootloader:      cp.GetSystemInfo().GetBootloader(),
		SDKVersion:      cp.GetSystemInfo().GetSdkVersion(),
		CellOperator:    cp.GetSystemInfo().GetNetworkOperator(),
		CountryCode:     cp.GetSystemInfo().GetCountryCode(),
		RawBatteryStats: extractCSV(cp.GetCheckin()),
	}
}

// saveWarningsAndErrors appends new errors (ne) and new warnings (nw) to existing slices
// and returns true if new errors were added
func saveWarningsAndErrors(warnings []string, nw string, errors []error, ne ...error) (bool, []string, []error) {
	newErr := false
	for _, e := range ne {
		if e != nil {
			errors = append(errors, e)
			newErr = true
		}
	}
	if len(nw) > 0 {
		warnings = append(warnings, nw)
	}
	return newErr, warnings, errors
}

// extractCSV extracts the checkin package information and last charged stats from the given input.
func extractCSV(input string) [][]string {
	// bs contains since last charged stats, separated by line and comma
	var bs [][]string
	// we only use since charged data (ignore unplugged data)
	for _, arr := range checkinutil.ParseCSV(input) {
		if len(arr) < minNumFields {
			continue
		}
		switch arr[2] {
		case info, sinceCharged:
			bs = append(bs, arr)
		}
	}
	return bs
}

// parseSection parses known checkin sections and returns true if the section was parsed.
// The app and system protos are directly modified.
func parseSection(c checkinutil.Counter, reportVersion, rawUID int32, section string, record []string, app *bspb.BatteryStats_App, system *bspb.BatteryStats_System, apkSeen map[apkID]bool, allAppComputedPowerMah *float32) (bool, string, []error) {
	switch section {
	case apkData:
		warn, errs := parseChildApk(c, record, app, apkSeen, rawUID)
		return true, warn, errs
	case audioData:
		if app.GetAudio() == nil {
			app.Audio = &bspb.BatteryStats_App_Audio{}
		}
		warn, errs := parseAndAccumulate(audioData, record, app.GetAudio())
		return true, warn, errs
	case batteryData:
		warn, errs := parseSystemBattery(c, record, system)
		return true, warn, errs
	case batteryDischargeData:
		warn, errs := parseSystemBatteryDischarge(c, record, system)
		return true, warn, errs
	case batteryLevelData:
		warn, errs := parseSystemBatteryLevel(c, record, system)
		return true, warn, errs
	case bluetoothControllerData:
		if app.GetBluetoothController() == nil {
			d, err := parseControllerData(c, bluetoothControllerData, record)
			if err != nil {
				return true, "", []error{err}
			}

			app.BluetoothController = d
			return true, "", nil
		}

		if err := parseAndAccumulateControllerData(c, bluetoothControllerData, record, app.BluetoothController); err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case cameraData:
		if app.GetCamera() == nil {
			app.Camera = &bspb.BatteryStats_App_Camera{}
		}
		warn, errs := parseAndAccumulate(cameraData, record, app.GetCamera())
		return true, warn, errs
	case chargeStepData:
		data, warn, err := parseStepData(record)
		if err != nil {
			return true, warn, []error{err}
		}

		system.ChargeStep = append(system.ChargeStep, &bspb.BatteryStats_System_ChargeStep{
			TimeMsec:      proto.Float32(data.timeMsec),
			Level:         proto.Float32(data.level),
			DisplayState:  data.displayState,
			PowerSaveMode: data.powerSaveMode,
			IdleMode:      data.idleMode,
		})
		return true, warn, nil
	case chargeTimeRemainData: // Current format: 9,0,i,ctr,18147528000
		// Case put here to acknowledge that we know about the section, but the information currently included is not useful for analysis.
		if len(record) > 1 {
			return true, fmt.Sprintf("%s now has additional data", chargeTimeRemainData), nil
		}
		return true, "", nil
	case cpuData:
		if app.GetCpu() == nil {
			app.Cpu = &bspb.BatteryStats_App_Cpu{}
		}
		warn, errs := parseAndAccumulate(cpuData, record, app.GetCpu())
		return true, warn, errs
	case dischargeStepData:
		data, warn, err := parseStepData(record)
		if err != nil {
			return true, warn, []error{err}
		}

		system.DischargeStep = append(system.DischargeStep, &bspb.BatteryStats_System_DischargeStep{
			TimeMsec:      proto.Float32(data.timeMsec),
			Level:         proto.Float32(data.level),
			DisplayState:  data.displayState,
			PowerSaveMode: data.powerSaveMode,
			IdleMode:      data.idleMode,
		})
		return true, warn, nil
	case dischargeTimeRemainData: // Current format: 9,0,i,dtr,18147528000
		// Case put here to acknowledge that we know about the section, but the information currently included is not useful for analysis.
		if len(record) > 1 {
			return true, fmt.Sprintf("%s now has additional data", dischargeTimeRemainData), nil
		}
		return true, "", nil
	case flashlightData:
		if app.GetFlashlight() == nil {
			app.Flashlight = &bspb.BatteryStats_App_Flashlight{}
		}
		warn, errs := parseAndAccumulate(flashlightData, record, app.GetFlashlight())
		return true, warn, errs
	case foregroundData:
		if app.GetForeground() == nil {
			app.Foreground = &bspb.BatteryStats_App_Foreground{}
		}
		warn, errs := parseAndAccumulate(foregroundData, record, app.GetForeground())
		return true, warn, errs
	case globalBluetoothControllerData:
		if reportVersion < 17 {
			warn, errs := parseGlobalBluetooth(record, system)
			return true, warn, errs
		}

		d, err := parseControllerData(c, globalBluetoothControllerData, record)
		if err != nil {
			return true, "", []error{err}
		}

		system.GlobalBluetoothController = d
		return true, "", nil
	case globalModemControllerData:
		d, err := parseControllerData(c, globalModemControllerData, record)
		if err != nil {
			return true, "", []error{err}
		}

		system.GlobalModemController = d
		return true, "", nil
	case globalNetworkData:
		warn, errs := parseGlobalNetwork(c, record, system)
		return true, warn, errs
	case globalWifiControllerData:
		d, err := parseControllerData(c, globalWifiControllerData, record)
		if err != nil {
			return true, "", []error{err}
		}

		system.GlobalWifiController = d
		return true, "", nil
	case globalWifiData:
		warn, errs := parseGlobalWifi(record, system)
		return true, warn, errs
	case jobData:
		warn, errs := parseAppScheduledJob(record, app)
		return true, warn, errs
	case kernelWakelockData:
		warn, errs := parseSystemKernelWakelock(record, system)
		return true, warn, errs
	case miscData:
		warn, errs := parseSystemMisc(c, reportVersion, record, system)
		return true, warn, errs
	case modemControllerData:
		if app.GetModemController() == nil {
			d, err := parseControllerData(c, modemControllerData, record)
			if err != nil {
				return true, "", []error{err}
			}

			app.ModemController = d
			return true, "", nil
		}

		if err := parseAndAccumulateControllerData(c, modemControllerData, record, app.ModemController); err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case networkData:
		warn, errs := parseAppNetwork(reportVersion, record, app)
		return true, warn, errs
	case powerUseItemData:
		warn, err := parseAppSystemPowerUseItem(c, record, app, system, allAppComputedPowerMah)
		if err != nil {
			return true, warn, []error{err}
		}
		return true, warn, nil
	case powerUseSummaryData:
		warn, errs := parseSystemPowerUseSummary(c, record, system)
		return true, warn, errs
	case processData:
		warn, errs := parseAppProcess(record, app)
		return true, warn, errs
	case screenBrightnessData:
		err := parseSystemScreenBrightness(c, record, system)
		if err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case sensorData:
		warn, errs := parseAppSensor(record, app)
		return true, warn, errs
	case signalScanningTimeData:
		warn, errs := parseSystemSignalScanningTime(c, record, system)
		return true, warn, errs
	case stateTimeData:
		warn, errs := parseAppStateTime(c, record, reportVersion, app)
		return true, warn, errs
	case syncData:
		warn, errs := parseAppSync(record, app)
		return true, warn, errs
	case userActivityData:
		if packageutils.AppID(rawUID) != 0 {
			if err := parseAppUserActivity(record, app); err != nil {
				return true, "", []error{err}
			}
		}
		return true, "", nil
	case vibratorData:
		if app.GetVibrator() == nil {
			app.Vibrator = &bspb.BatteryStats_App_Vibrator{}
		}
		warn, errs := parseAndAccumulate(vibratorData, record, app.GetVibrator())
		return true, warn, errs
	case videoData:
		if app.GetVideo() == nil {
			app.Video = &bspb.BatteryStats_App_Video{}
		}
		warn, errs := parseAndAccumulate(videoData, record, app.GetVideo())
		return true, warn, errs
	case wakelockData:
		warn, errs := parseAppWakelock(record, app)
		return true, warn, errs
	case wakeupReasonData:
		warn, errs := parseSystemWakeupReason(record, system)
		return true, warn, errs
	case wifiControllerData:
		if app.GetWifiController() == nil {
			d, err := parseControllerData(c, wifiControllerData, record)
			if err != nil {
				return true, "", []error{err}
			}

			app.WifiController = d
			return true, "", nil
		}

		err := parseAndAccumulateControllerData(c, wifiControllerData, record, app.WifiController)
		if err != nil {
			return true, "", []error{err}
		}
		return true, "", nil
	case wifiData:
		warn, errs := parseAppWifi(record, app)
		return true, warn, errs
	case signalStrengthTimeData, signalStrengthCountData, dataConnectionTimeData, dataConnectionCountData, wifiStateTimeData, wifiStateCountData, bluetoothStateTimeData, bluetoothStateCountData, wifiSupplStateTimeData, wifiSupplStateCountData, wifiSignalStrengthTimeData, wifiSignalStrengthCountData:
		warn, err := parseSystemTimeCountPair(c, section, record, system)
		if err != nil {
			return true, warn, []error{err}
		}
		return true, warn, nil
	default:
		return false, "", nil
	}
}

// postProcessBatteryStats handles processing and data population of special fields.
func postProcessBatteryStats(bs *bspb.BatteryStats) {
	if bs == nil {
		return
	}
	sys := bs.System
	if sys == nil {
		return
	}
	// Charge and discharge step info is printed out (and thus read) in reverse order
	// (low to high for discharge and high to low for charge) so reverse the order to make more sense.
	if cs := sys.GetChargeStep(); cs != nil {
		for i := 0; i < len(cs)/2; i++ {
			j := len(cs) - i - 1
			cs[i], cs[j] = cs[j], cs[i]
		}
	}
	if ds := sys.GetDischargeStep(); ds != nil {
		for i := 0; i < len(ds)/2; i++ {
			j := len(ds) - i - 1
			ds[i], ds[j] = ds[j], ds[i]
		}
	}

	// Convenience functions to avoid repeating code.
	makeGWC := func() {
		if gw := sys.GlobalWifi; gw != nil {
			sys.GlobalWifiController = &bspb.BatteryStats_ControllerActivity{
				IdleTimeMsec: proto.Int64(int64(gw.GetWifiIdleTimeMsec())),
				RxTimeMsec:   proto.Int64(int64(gw.GetWifiRxTimeMsec())),
				PowerMah:     proto.Int64(int64(gw.GetWifiPowerMah())),
				Tx: []*bspb.BatteryStats_ControllerActivity_TxLevel{
					{
						Level:    proto.Int32(0),
						TimeMsec: proto.Int64(int64(gw.GetWifiTxTimeMsec())),
					},
				},
			}
		}
	}
	makeGB := func() {
		if gbc := sys.GlobalBluetoothController; gbc != nil {
			gb := &bspb.BatteryStats_System_GlobalBluetooth{}
			gb.BluetoothIdleTimeMsec = proto.Float32(float32(gbc.GetIdleTimeMsec()))
			gb.BluetoothRxTimeMsec = proto.Float32(float32(gbc.GetRxTimeMsec()))
			gb.BluetoothPowerMah = proto.Float32(float32(gbc.GetPowerMah()))
			var t int64
			for _, tx := range gbc.Tx {
				t += tx.GetTimeMsec()
			}
			gb.BluetoothTxTimeMsec = proto.Float32(float32(t))
			sys.GlobalBluetooth = gb
		}
	}

	// This is all in one function to provide context and because there wasn't
	// a clear line to allow breaking things into smaller functions.
	// TODO: put things into smaller functions if there are clearer lines.
	if bs.GetReportVersion() >= 17 {
		if sys.GlobalWifiController != nil {
			// This report version is post-controller activity change.
			// Populate incorrect/missing data in old fields.
			gw := sys.GlobalWifi
			if gw == nil {
				gw = &bspb.BatteryStats_System_GlobalWifi{}
				sys.GlobalWifi = gw
			}
			gw.WifiIdleTimeMsec = proto.Float32(float32(sys.GlobalWifiController.GetIdleTimeMsec()))
			gw.WifiRxTimeMsec = proto.Float32(float32(sys.GlobalWifiController.GetRxTimeMsec()))
			gw.WifiPowerMah = proto.Float32(float32(sys.GlobalWifiController.GetPowerMah()))
			var t int64
			for _, tx := range sys.GlobalWifiController.Tx {
				t += tx.GetTimeMsec()
			}
			gw.WifiTxTimeMsec = proto.Float32(float32(t))

			// Going to use the presence of GlobalWifiController to indicate that the gble data
			// is controller activity data. Not perfect, but there's no guarantee in version 17
			// about what kind it is, and the power and tx_time fields were swapped, so we must
			// [try to] fix them.
			makeGB()
		} else {
			makeGWC()

			if gbc := sys.GlobalBluetoothController; gbc != nil {
				if len(gbc.Tx) == 1 {
					// GlobalWifiController is nil and there was only one transmit level detected.
					// Assume it's the old gble format. Must swap the power and tx_time data.
					cPwr, cTx := gbc.GetTx()[0].TimeMsec, gbc.PowerMah
					gbc.PowerMah, gbc.GetTx()[0].TimeMsec = cPwr, cTx
				}
				makeGB()
			}
		}
	}
	if bs.GetReportVersion() < 17 {
		// Copy values into newer protos so that analyses can rely more on the newer protos.
		if gb := sys.GlobalBluetooth; gb != nil {
			sys.GlobalBluetoothController = &bspb.BatteryStats_ControllerActivity{
				IdleTimeMsec: proto.Int64(int64(gb.GetBluetoothIdleTimeMsec())),
				RxTimeMsec:   proto.Int64(int64(gb.GetBluetoothRxTimeMsec())),
				PowerMah:     proto.Int64(int64(gb.GetBluetoothPowerMah())),
				Tx: []*bspb.BatteryStats_ControllerActivity_TxLevel{
					{
						Level:    proto.Int32(0),
						TimeMsec: proto.Int64(int64(gb.GetBluetoothTxTimeMsec())),
					},
				},
			}
		}

		makeGWC()
	}
	if m := sys.Misc; m != nil {
		if bs.GetReportVersion() >= 14 {
			// These won't have been populated through regular parsing for version 14+ so it's
			// safe to overwrite here.
			if gw := sys.GlobalWifi; gw != nil {
				m.WifiOnTimeMsec = gw.WifiOnTimeMsec
				m.WifiRunningTimeMsec = gw.WifiRunningTimeMsec
			}
			if gn := sys.GlobalNetwork; gn != nil {
				m.MobileBytesRx = gn.MobileBytesRx
				m.MobileBytesTx = gn.MobileBytesTx
				m.WifiBytesRx = gn.WifiBytesRx
				m.WifiBytesTx = gn.WifiBytesTx
			}
		} else {
			// sys.GlobalWifi won't have been populated through regular parsing for version 13-
			// so it's safe to overwrite here.
			sys.GlobalWifi = &bspb.BatteryStats_System_GlobalWifi{
				WifiOnTimeMsec:      m.WifiOnTimeMsec,
				WifiRunningTimeMsec: m.WifiRunningTimeMsec,
			}
			// Mobile and wifi bytes were being printed in both misc and GlobalNetwork lines for
			// report versions 7-13 so no need to re-assign sys.GlobalNetwork.WifiBytes* and
			// sys.GlobalNetwork.MobileBytes* here.
		}
	}
}

// GroupName returns a predefined UID group name for the given sharedUserID label.
// If there is no predefined name, then an empty string is returned.
func GroupName(sharedUIDLabel string) string {
	return sharedUIDLabelMap[sharedUIDLabel]
}

// parseStepData parses sliced lines that were orginally in the form 9,0,i,csd,65975,40,sd,p-
// Use this to parse lines for CHARGE_STEP_DATA (csd) and DISCHARGE_STEP_DATA (dsd).
func parseStepData(r []string) (*stepData, string, error) {
	var err error
	var warn string
	var tm, l float32
	var ds bspb.BatteryStats_System_DisplayState_State
	var psm bspb.BatteryStats_System_PowerSaveMode_Mode
	var im bspb.BatteryStats_System_IdleMode_Mode

	if len(r) > 0 {
		tm, err = parseFloat32(r[0])
		if err != nil {
			return nil, warn, err
		}
	}

	if len(r) > 1 {
		// A ? indicates a bug/error with the display state in the log and so we should treat it as such.
		// The ? was intended for index 2, but was in index 1 due to a (now fixed) bug.
		if r[1] == "?" {
			return nil, warn, errors.New("discovered ? for display state")
		}
		l, err = parseFloat32(r[1])
		if err != nil {
			return nil, warn, err
		}
	}

	ds = bspb.BatteryStats_System_DisplayState_MIXED
	if len(r) > 2 {
		switch r[2] {
		case "s+":
			ds = bspb.BatteryStats_System_DisplayState_ON
		case "s-":
			ds = bspb.BatteryStats_System_DisplayState_OFF
		case "sd":
			ds = bspb.BatteryStats_System_DisplayState_DOZE
		case "sds":
			ds = bspb.BatteryStats_System_DisplayState_DOZE_SUSPEND
		case "": // Empty strings are valid.
			ds = bspb.BatteryStats_System_DisplayState_MIXED
		case "?": // A ? indicates a bug/error in the log and so we should treat it as such.
			return nil, warn, errors.New("discovered ? when parsing display state")
		default:
			warn = fmt.Sprintf("unknown display state: %s", r[2])
		}
	}

	psm = bspb.BatteryStats_System_PowerSaveMode_MIXED
	im = bspb.BatteryStats_System_IdleMode_NO_DATA
	if len(r) > 3 {
		switch r[3] {
		case "p+":
			psm = bspb.BatteryStats_System_PowerSaveMode_ON
		case "p-":
			psm = bspb.BatteryStats_System_PowerSaveMode_OFF
		case "": // Empty strings are valid.
			psm = bspb.BatteryStats_System_PowerSaveMode_MIXED
		// i+ and i- were in the 4th slot due to a (now fixed) bug.
		case "i+":
			im = bspb.BatteryStats_System_IdleMode_ON
		case "i-":
			im = bspb.BatteryStats_System_IdleMode_OFF
		default:
			warn = fmt.Sprintf("unknown power save mode: %s", r[3])
		}
	}

	if len(r) > 4 {
		switch r[4] {
		case "i+":
			im = bspb.BatteryStats_System_IdleMode_ON
		case "i-":
			im = bspb.BatteryStats_System_IdleMode_OFF
		case "": // Empty strings are valid.
			im = bspb.BatteryStats_System_IdleMode_MIXED
		default:
			warn = fmt.Sprintf("unknown idle mode: %s", r[4])
		}
	}

	return &stepData{
		timeMsec:      tm,
		level:         l,
		displayState:  &ds,
		powerSaveMode: &psm,
		idleMode:      &im,
	}, warn, nil
}

// processAppInfo tries to determine the best name and version_code to assign
// to the BatteryStats_App element of each shared uid.
func processAppInfo(uids map[int32]*bspb.BatteryStats_App, suidMap map[int32]string) []error {
	var errs []error

	for uid, app := range uids {
		known := false
		var headChild *bspb.BatteryStats_App_Child
		var childNames []string

		suid := suidMap[uid]
		if l := GroupName(suid); l != "" {
			app.Name = proto.String(l)
			// Only mark known if the shared UID label has a predefined name.
			// We will use the shared UID label itself later on if there are
			// multiple packages in this proto.
			known = true
		}

		for _, child := range app.Child {
			childNames = append(childNames, child.GetName())

			// Check to see if it's any of the other shared UIDs.
			if val, ok := packageNameToSharedUIDMap[child.GetName()]; !known && ok {
				app.Name = proto.String(val)
				known = true
			} else if known && ok && app.GetName() != val {
				errs = append(errs, fmt.Errorf("package groups not matched properly"))
			}
		}

		app.Uid = proto.Int32(uid)
		if name, ok := KnownUIDs[uid]; ok {
			app.Name = proto.String(name)
			known = true
		}

		if len(app.Child) == 1 && headChild == nil {
			headChild = app.GetChild()[0]
		}

		if headChild != nil {
			app.VersionCode = headChild.VersionCode
			app.VersionName = headChild.VersionName
		}

		// The children aren't part of any known shared UIDs.
		// Creating a name based on the concatenation of children's names, as defined in ../batterystats/parse.go
		if !known {
			if suid != "" && len(childNames) != 1 {
				app.Name = proto.String(fmt.Sprintf("SharedUserID(%s)", suid))
				continue
			}
			prefix := ""
			if len(childNames) == 0 && len(app.GetProcess()) > 0 {
				// No children...process names may include clues as to what this UID was for.
				for _, p := range app.GetProcess() {
					childNames = append(childNames, p.GetName())
				}
				prefix = "process:"
			}
			sort.Strings(childNames) // Needed for consistent ordering
			app.Name = proto.String(prefix + strings.Join(childNames, "|"))
		}
	}

	return errs
}

// parsePackageManager parses the uid section of the checkin report.
// It builds a map from UID to the relevant BatteryStats_App proto.
// All packages that share the same UID are listed in the proto's list
// of children. The rest of the proto is filled in later.
func parsePackageManager(pc checkinutil.Counter, bs [][]string, pkgs []*usagepb.PackageInfo) (map[int32]*bspb.BatteryStats_App, []error) {
	// Use a map to ensure we don't double count packages.
	apps := make(map[string]*usagepb.PackageInfo)
	for _, p := range pkgs {
		apps[p.GetPkgName()] = p
	}

	uids := make(map[int32]*bspb.BatteryStats_App)
	var errs []error

	for _, r := range bs {
		// Expect inputs like "8,0,i,uid,1000,com.android.settings".
		if r[3] != uidData {
			continue
		}
		var uid int32
		var name string
		if _, err := parseSlice(pc, r[3], r[4:], &uid, &name); err != nil {
			errs = append(errs, fmt.Errorf("error parsing PackageManager: %v", err))
			continue
		}
		uid = packageutils.AppID(uid)
		if _, known := apps[name]; !known {
			apps[name] = &usagepb.PackageInfo{
				PkgName: proto.String(name),
				Uid:     proto.Int32(uid),
			}
		} else if packageutils.AppID(apps[name].GetUid()) == 0 {
			// It looks like a lot of packages have a uid of 0 coming from the device.
			// TODO: figure out why we're getting UIDs of 0 on the device side.
			apps[name].Uid = proto.Int32(uid)
		}
	}

	// We've gone through both sources of package/uid info; now we can create the App and Child elements.
	for _, p := range apps {
		uid := packageutils.AppID(p.GetUid())
		if uid == 0 {
			// Skip packages uploaded with a UID of 0 (this would be the case for old builds).
			continue
		}
		stats := uids[uid]
		if stats == nil {
			stats = &bspb.BatteryStats_App{
				Uid: proto.Int32(uid),
			}
			uids[uid] = stats
		}
		stats.Child = append(stats.Child,
			&bspb.BatteryStats_App_Child{
				Name:        p.PkgName,
				VersionCode: p.VersionCode,
				VersionName: p.VersionName,
			})
	}

	return uids, errs
}

// apkID identifies App.Apk whose App can have multiple UIDs due to multi-user support.
type apkID struct {
	// UID specified in raw BatteryStats data, not the result of modulo operation (% 100000) for
	// multi-user (e.g., 1110066 instead of the actual UID 10066)
	RawUID  int32
	PkgName string
}

// parseChildApk parses "apk" (APK_DATA) into the relevant child proto in app.
//
// If there exists a service entry with the same name, newly found values (e.g. StartTimeMsec) from
// parsing will be added to their corresponding fields in that entry. Otherwise, a new entry will be created.
//
// apkSeen maintains apkIDs whose Apk.Wakeups were correctly handled already.
// rawUID is the raw UID as explained in apkID.
//
// format: 8,1000,l,apk,5719,android,android.hardware.location.GeofenceHardwareService,0,0,2
// wakeup alarms, Apk name, service name, time spent started, start count, launch count
func parseChildApk(pc checkinutil.Counter, record []string, app *bspb.BatteryStats_App, apkSeen map[apkID]bool, rawUID int32) (string, []error) {
	var name string
	var wakeups float32
	// "wakeups" and "name" are shared across services of the same child apk.
	record, err := parseSlice(pc, apkData, record, &wakeups, &name)
	if err != nil {
		return "", []error{err}
	}
	var c *bspb.BatteryStats_App_Child
	// pkg name should have been added when parsing package manager("uid").
	for _, child := range app.GetChild() {
		if child.GetName() == name {
			c = child
			break
		}
	}
	if c == nil {
		if app.HeadChild != nil && app.GetHeadChild().GetName() == name {
			c = app.HeadChild
		} else {
			c = &bspb.BatteryStats_App_Child{Name: proto.String(name)}
			app.Child = append(app.Child, c)
		}
	}

	if id := (apkID{rawUID, name}); !apkSeen[id] { // We need to process "wakeups" only once per rawUID/pkgName pair.
		apk := c.GetApk()
		if apk == nil {
			apk = &bspb.BatteryStats_App_Apk{}
			c.Apk = apk
		}
		apk.Wakeups = proto.Float32(apk.GetWakeups() + wakeups)
		apkSeen[id] = true
	}

	s := &bspb.BatteryStats_App_Apk_Service{}
	warn, plErrs := parseLine(apkData, record, s)
	if len(plErrs) > 0 {
		return warn, plErrs
	}

	// Check to see if we've already encountered a service of the same name for this particular child.
	// If so, then increment the parsed values.
	for _, s1 := range c.Apk.Service {
		if s1.GetName() == s.GetName() {
			s1.StartTimeMsec = proto.Float32(s1.GetStartTimeMsec() + s.GetStartTimeMsec())
			s1.Starts = proto.Float32(s1.GetStarts() + s.GetStarts())
			s1.Launches = proto.Float32(s1.GetLaunches() + s.GetLaunches())
			return warn, nil
		}
	}

	// s wasn't found in the child's list of services so add it as a new one.
	c.Apk.Service = append(c.Apk.Service, s)
	return warn, nil
}

// aggregateAppApk iterates through the app's child list and aggregates the apk data of each child.
// The result is saved into app.Apk.
func aggregateAppApk(app *bspb.BatteryStats_App) {
	apk := &bspb.BatteryStats_App_Apk{}
	services := make(map[string]*bspb.BatteryStats_App_Apk_Service)
	headCounted := app.HeadChild == nil
	for _, child := range app.GetChild() {
		if child.Apk != nil {
			if child.GetName() == app.GetHeadChild().GetName() {
				headCounted = true
			}
			apk.Wakeups = proto.Float32(apk.GetWakeups() + child.GetApk().GetWakeups())
			for _, service := range child.GetApk().GetService() {
				if appServ, ok := services[service.GetName()]; ok {
					appServ.StartTimeMsec = proto.Float32(appServ.GetStartTimeMsec() + service.GetStartTimeMsec())
					appServ.Starts = proto.Float32(appServ.GetStarts() + service.GetStarts())
					appServ.Launches = proto.Float32(appServ.GetLaunches() + service.GetLaunches())
				} else {
					services[service.GetName()] = service
				}
			}
		}
	}
	h := app.GetHeadChild()
	if !headCounted && h.Apk != nil {
		apk.Wakeups = proto.Float32(apk.GetWakeups() + h.GetApk().GetWakeups())
		for _, service := range h.GetApk().GetService() {
			if appServ, ok := services[service.GetName()]; ok {
				appServ.StartTimeMsec = proto.Float32(appServ.GetStartTimeMsec() + service.GetStartTimeMsec())
				appServ.Starts = proto.Float32(appServ.GetStarts() + service.GetStarts())
				appServ.Launches = proto.Float32(appServ.GetLaunches() + service.GetLaunches())
			} else {
				services[service.GetName()] = service
			}
		}
	}

	if len(services) == 0 {
		// There were no apk lines for this UID.
		return
	}

	for _, s := range services {
		apk.Service = append(apk.Service, s)
	}

	app.Apk = apk
}

// parseAppProcess parses "pr" (PROCESS_DATA) in app.
//
// If there exists a process with the same name, newly found values (e.g. UserTimeMsec) from
// parsing will be added to their corresponding fields in that process. Otherwise, a new
// process will be created.
//
// record holds content from BatteryStats's processData.
//   e.g., com.google.android.music:main,0,10,0,0
func parseAppProcess(record []string, app *bspb.BatteryStats_App) (string, []error) {
	p := &bspb.BatteryStats_App_Process{}
	warn, errs := parseLine(processData, record, p)
	if len(errs) > 0 {
		return warn, errs
	}

	for _, p1 := range app.Process {
		if p1.GetName() == p.GetName() {
			p1.UserTimeMsec = proto.Float32(p1.GetUserTimeMsec() + p.GetUserTimeMsec())
			p1.SystemTimeMsec = proto.Float32(p1.GetSystemTimeMsec() + p.GetSystemTimeMsec())
			p1.ForegroundTimeMsec = proto.Float32(p1.GetForegroundTimeMsec() + p.GetForegroundTimeMsec())
			p1.Starts = proto.Float32(p1.GetStarts() + p.GetStarts())
			p1.Anrs = proto.Float32(p1.GetAnrs() + p.GetAnrs())
			p1.Crashes = proto.Float32(p1.GetCrashes() + p.GetCrashes())
			return warn, nil
		}
	}

	// p wasn't found in app's list of processes so add it as a new one.
	app.Process = append(app.Process, p)
	return warn, nil
}

// parseAppUserActivity parses "ua" (USER_ACTIVITY_TYPES) in App.
// format: 8,1000,l,ua,2,0,0
// "other", "button", "touch" activity counts
func parseAppUserActivity(record []string, app *bspb.BatteryStats_App) error {
	for name, rawCount := range record {
		Count, err := parseFloat32(rawCount)
		if err != nil {
			return err
		}
		app.UserActivity = append(app.UserActivity,
			&bspb.BatteryStats_App_UserActivity{
				Name:  bspb.BatteryStats_App_UserActivity_Name(name).Enum(),
				Count: proto.Float32(Count),
			})
	}
	return nil
}

// parseAppSensor parses "sr" (SENSOR_DATA) in App.
//
// If there exists a sensor entry with the same number, newly found values (e.g. TotalTimeMsec) from
// parsing will be added to their corresponding fields in that entry. Otherwise, a new entry will be created.
//
// record holds content from BatteryStats's sensorData.
//   e.g., 5,45859,3
func parseAppSensor(record []string, app *bspb.BatteryStats_App) (string, []error) {
	s := &bspb.BatteryStats_App_Sensor{}
	warn, errs := parseLine(sensorData, record, s)
	if len(errs) > 0 {
		return warn, errs
	}

	for _, s1 := range app.Sensor {
		if s1.GetNumber() == s.GetNumber() {
			s1.TotalTimeMsec = proto.Float32(s1.GetTotalTimeMsec() + s.GetTotalTimeMsec())
			s1.Count = proto.Float32(s1.GetCount() + s.GetCount())
			return warn, nil
		}
	}

	// s wasn't found in app's list of sensor usage so add it as a new one.
	app.Sensor = append(app.Sensor, s)
	return warn, nil
}

// parseSystemPowerUseSummary parses "pws" (POWER_USE_SUMMARY_DATA) in System.
//
// record holds content from BatteryStats's powerUseSummaryData.
//   e.g., 2100,76.7,84.0,105
func parseSystemPowerUseSummary(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetPowerUseSummary() != nil {
		pc.Count("error-parse-system-power-use-summary-exist", 1)
		return "", []error{errors.New("power use summary field already exists")}
	}
	p := &bspb.BatteryStats_System_PowerUseSummary{}
	warn, errs := parseLine(powerUseSummaryData, record, p)
	if len(errs) == 0 {
		system.PowerUseSummary = p
	}
	return warn, errs
}

// parseAppSystemPowerUseItem parses "pwi" (POWER_USE_ITEM_DATA) in App and System's PowerUseItem.
// We need to match raw item name with proto item name, so we parse manually.
// format: 8,0,u,pwi,unacc,2277
// drain type, power in mAh
// If drain type is "uid", it's per-app data, we add power in mAh into its app proto
// If drain type matches other types specified in "powerUseItemNameMap", we add
// it to system proto.
// If app has a PowerUseItem already, then newly found values from parsing will be added to it.
// The most common case of this would be when an app is installed on a device with multiple users
// (ie. with a work profile). In such cases, the app uid is combined with the user ID (to create
// UIDs such as 1010011 vs 10011) and thus the app is treated and reported separately for each profile.
// TODO: we currently combine them all under the same UID. In the future, we should separate them.
// The sum of app consumed power will be added to system proto later.
func parseAppSystemPowerUseItem(pc checkinutil.Counter, record []string, app *bspb.BatteryStats_App, system *bspb.BatteryStats_System, allAppComputedPowerMah *float32) (string, error) {
	var rawName string
	var computedPowerMah float32
	if _, err := parseSlice(pc, powerUseItemData, record, &rawName, &computedPowerMah); err != nil {
		return "", err
	}
	if rawName == "uid" {
		*allAppComputedPowerMah += computedPowerMah
		pb := app.GetPowerUseItem()
		if pb == nil {
			pb = &bspb.BatteryStats_App_PowerUseItem{}
			app.PowerUseItem = pb
		}
		pb.ComputedPowerMah = proto.Float32(pb.GetComputedPowerMah() + computedPowerMah)
	} else if name, ok := powerUseItemNameMap[rawName]; ok {
		system.PowerUseItem = append(system.PowerUseItem,
			&bspb.BatteryStats_System_PowerUseItem{
				Name:             name.Enum(),
				ComputedPowerMah: proto.Float32(computedPowerMah),
			})
	} else {
		pc.Count("unknown-app-system-power-use-item-"+rawName, 1)
		return fmt.Sprintf("Unknown powerUseItem name %s", rawName), nil
	}
	return "", nil
}

// parseAppScheduledJob parses "jb" (JOB_DATA) in App.
//
// If there exists a scheduledJob with the same name, newly found values
// (e.g., TotalTimeMsec) from parsing will be accumulated to their corresponding fields in that
// scheduledJob. Otherwise, a new scheduledJob will be created.
//
// format: 9,10007,l,jb,com.google.android.gms/.gcm.nts.TaskExecutionService,14000,3
// name, total time (msec), count
func parseAppScheduledJob(record []string, app *bspb.BatteryStats_App) (string, []error) {
	j := &bspb.BatteryStats_App_ScheduledJob{}
	warn, errs := parseLine(jobData, record, j)
	if len(errs) > 0 {
		return warn, errs
	}

	for _, j1 := range app.ScheduledJob {
		if j1.GetName() == j.GetName() {
			j1.TotalTimeMsec = proto.Float32(j1.GetTotalTimeMsec() + j.GetTotalTimeMsec())
			j1.Count = proto.Float32(j1.GetCount() + j.GetCount())
			return warn, nil
		}
	}

	// Job isn't in app's list of scheduled jobs so we add it as a new one
	app.ScheduledJob = append(app.ScheduledJob, j)
	return warn, nil
}

// parseAppStateTime parses "st" (STATE_TIME_DATA) in App.
//
// If the app already has state time data, newly found values from parsing will be accumulated to
// their corresponding fields.
//
// format:
// 9,10007,l,st,10,20,30 (report version <= 16)
//   [foreground time (ms), active time (ms), running/cached time (ms)]
// 9,10007,l,st,10,20,30,40,50,60 (report version >= 17)
//   [top time, foreground service time, top sleeping time, foreground time, background time, cached time]
func parseAppStateTime(pc checkinutil.Counter, record []string, reportVersion int32, app *bspb.BatteryStats_App) (string, []error) {
	if app.GetStateTime() == nil {
		app.StateTime = &bspb.BatteryStats_App_StateTime{}
	}
	if reportVersion < 17 {
		return parseAndAccumulate(stateTimeData, record, app.GetStateTime())
	}
	var fore, cached float32 // Proto backwards compatibility...these were float32 before.
	var top, foreServ, topSleep, back int64
	rem, err := parseSlice(pc, stateTimeData, record, &top, &foreServ, &topSleep, &fore, &back, &cached)
	var warn string
	if len(rem) > 0 {
		warn = fmt.Sprintf("%s has %d new fields", stateTimeData, len(rem))
	}
	if err != nil {
		return warn, []error{err}
	}

	s := app.GetStateTime()
	s.TopTimeMsec = proto.Int64(s.GetTopTimeMsec() + top)
	s.ForegroundServiceTimeMsec = proto.Int64(s.GetForegroundServiceTimeMsec() + foreServ)
	s.TopSleepingTimeMsec = proto.Int64(s.GetTopSleepingTimeMsec() + topSleep)
	s.ForegroundTimeMsec = proto.Float32(s.GetForegroundTimeMsec() + fore)
	s.BackgroundTimeMsec = proto.Int64(s.GetBackgroundTimeMsec() + back)
	s.CachedTimeMsec = proto.Float32(s.GetCachedTimeMsec() + cached)

	return warn, nil
}

// parseAppSync parses "sy" (SYNC_DATA) in App.
// If there exists a sync with the same name, newly found values (e.g. TotalTimeMsec) from
// parsing will be added to their corresponding fields in that sync. Otherwise, a new sync
// will be created.
//
// format: 8,10007,l,sy,com.google.android.gms.games/com.google/...@gmail.com,2161,4
// name, total time locked, count
func parseAppSync(record []string, app *bspb.BatteryStats_App) (string, []error) {
	s := &bspb.BatteryStats_App_Sync{}
	warn, plErrs := parseLine(syncData, record, s)
	if len(plErrs) > 0 {
		return warn, plErrs
	}
	// Scrub PII from the sync name
	s.Name = proto.String(historianutils.ScrubPII(s.GetName()))
	for _, s1 := range app.Sync {
		if s1.GetName() == s.GetName() {
			s1.TotalTimeMsec = proto.Float32(s1.GetTotalTimeMsec() + s.GetTotalTimeMsec())
			s1.Count = proto.Float32(s1.GetCount() + s.GetCount())
			return warn, nil
		}
	}
	// Sync wasn't found in app's list of syncs so add it as a new one
	app.Sync = append(app.Sync, s)
	return warn, nil
}

// parseAppWakelock parses "wl" (WAKELOCK_DATA) in App.
// If there exists a wakelock with the same name, newly found values (e.g. FullTimeMsec)
// from parsing will be added to their corresponding fields in that wakelock.
// Otherwise, a new wakelock will be created.
//
// format: 8,1000,l,wl,ConnectivityService,0,f,0,15411273,p,263,0,w,0
// name, full wakelock time,    "f" (for full),    full wakelock count,
//       partial wakelock time, "p" (for partial), partial wakelock count
//       window wakelock time,  "w" (for window),  window wakelock count
func parseAppWakelock(record []string, app *bspb.BatteryStats_App) (string, []error) {
	w := &bspb.BatteryStats_App_Wakelock{}
	// The line contains letters that represent wakelock types, we skip those fields.
	warn, plErrs := parseLineWithSkip(wakelockData, record, w, []int{2 /*"f"*/, 5 /*"p"*/, 8 /*"w"*/})
	if len(plErrs) > 0 {
		return warn, plErrs
	}
	// Scrub the PII from the wakelock data
	w.Name = proto.String(historianutils.ScrubPII(w.GetName()))
	for _, w1 := range app.Wakelock {
		if w1.GetName() == w.GetName() {
			w1.FullTimeMsec = proto.Float32(w1.GetFullTimeMsec() + w.GetFullTimeMsec())
			w1.FullCount = proto.Float32(w1.GetFullCount() + w.GetFullCount())
			w1.PartialTimeMsec = proto.Float32(w1.GetPartialTimeMsec() + w.GetPartialTimeMsec())
			w1.PartialCount = proto.Float32(w1.GetPartialCount() + w.GetPartialCount())
			w1.WindowTimeMsec = proto.Float32(w1.GetWindowTimeMsec() + w.GetWindowTimeMsec())
			w1.WindowCount = proto.Float32(w1.GetWindowCount() + w.GetWindowCount())
			return warn, nil
		}
	}

	// Wakelock wasn't found in app's list of wakelocks so add it as a new one.
	app.Wakelock = append(app.Wakelock, w)
	return warn, nil
}

// parseAppNetwork parses "nt" (NETWORK_DATA) into App.
//
// record holds content from BatteryStats's networkData.
//   e.g., 0,0,996,1389 (reportVersion < 8)
//   e.g., 0,0,8987,7051,0,0,25,29,0,0 (reportVersion >= 8)
// If app has a Network field already, then newly found values from parsing will be added to it.
// The most common case of this would be when an app is installed on a device with multiple users
// (ie. with a work profile). In such cases, the app uid is combined with the user ID (to create
// UIDs such as 1010011 vs 10011) and thus the app is treated and reported separately for each profile.
func parseAppNetwork(reportVersion int32, record []string, app *bspb.BatteryStats_App) (string, []error) {
	n := &bspb.BatteryStats_App_Network{}
	warn, errs := parseLine(networkData, record, n)
	if len(errs) > 0 {
		return warn, errs
	}
	// MobileActiveTime is output in microseconds in the log, so we convert to milliseconds here.
	n.MobileActiveTimeMsec = proto.Float32(n.GetMobileActiveTimeMsec() / 1e3)

	pb := app.GetNetwork()
	if pb == nil {
		app.Network = n
		return warn, nil
	}

	pb.MobileBytesRx = proto.Float32(pb.GetMobileBytesRx() + n.GetMobileBytesRx())
	pb.MobileBytesTx = proto.Float32(pb.GetMobileBytesTx() + n.GetMobileBytesTx())
	pb.WifiBytesRx = proto.Float32(pb.GetWifiBytesRx() + n.GetWifiBytesRx())
	pb.WifiBytesTx = proto.Float32(pb.GetWifiBytesTx() + n.GetWifiBytesTx())
	pb.MobilePacketsRx = proto.Float32(pb.GetMobilePacketsRx() + n.GetMobilePacketsRx())
	pb.MobilePacketsTx = proto.Float32(pb.GetMobilePacketsTx() + n.GetMobilePacketsTx())
	pb.WifiPacketsRx = proto.Float32(pb.GetWifiPacketsRx() + n.GetWifiPacketsRx())
	pb.WifiPacketsTx = proto.Float32(pb.GetWifiPacketsTx() + n.GetWifiPacketsTx())
	pb.MobileActiveTimeMsec = proto.Float32(pb.GetMobileActiveTimeMsec() + n.GetMobileActiveTimeMsec())
	pb.MobileActiveCount = proto.Float32(pb.GetMobileActiveCount() + n.GetMobileActiveCount())

	return warn, nil
}

// parseAppWifi parses "wfl" (WIFI_DATA) in App.
// Parsing manually due to unit conversion. If app has a Wifi field already,
// then newly found values from parsing will be added to it.
// format: 14,10009,l,wfl,1386709324,304313000,0,3000,1500,500
// full wifi lock on time (usec), wifi scan time (usec), app wifi running time (usec), wifi idle time (msec), wifi Rx time (msec), wifi Tx time (msec)
func parseAppWifi(record []string, app *bspb.BatteryStats_App) (string, []error) {
	if len(record) < 3 {
		return "", []error{fmt.Errorf("%s line didn't contain enough fields", wifiData)}
	}

	w := &bspb.BatteryStats_App_Wifi{}
	warn, errs := parseLine(wifiData, record, w)
	if len(errs) > 0 {
		return warn, errs
	}

	// fullWifiLockTime, scanTime, runningTime are reported in usec, not msec, we convert them to msec here
	w.FullWifiLockTimeMsec = proto.Float32(w.GetFullWifiLockTimeMsec() / 1e3)
	w.ScanTimeMsec = proto.Float32(w.GetScanTimeMsec() / 1e3)
	w.RunningTimeMsec = proto.Float32(w.GetRunningTimeMsec() / 1e3)

	pb := app.GetWifi()
	if pb == nil {
		app.Wifi = w
		return warn, nil
	}

	pb.FullWifiLockTimeMsec = proto.Float32(pb.GetFullWifiLockTimeMsec() + w.GetFullWifiLockTimeMsec())
	pb.ScanTimeMsec = proto.Float32(pb.GetScanTimeMsec() + w.GetScanTimeMsec())
	pb.RunningTimeMsec = proto.Float32(pb.GetRunningTimeMsec() + w.GetRunningTimeMsec())
	pb.ScanCount = proto.Float32(pb.GetScanCount() + w.GetScanCount())
	pb.IdleTimeMsec = proto.Float32(pb.GetIdleTimeMsec() + w.GetIdleTimeMsec())
	pb.RxTimeMsec = proto.Float32(pb.GetRxTimeMsec() + w.GetRxTimeMsec())
	pb.TxTimeMsec = proto.Float32(pb.GetTxTimeMsec() + w.GetTxTimeMsec())
	return warn, nil
}

// parseControllerData parses any output that describes controller activity.
//
// format: <idle_time>, <rx_time>, <power_ma_ms>, tx_time...
func parseControllerData(pc checkinutil.Counter, section string, record []string) (*bspb.BatteryStats_ControllerActivity, error) {
	var idle, rx, pwr int64
	rem, err := parseSlice(pc, section, record, &idle, &rx, &pwr)
	if err != nil {
		return nil, err
	}
	if len(rem) == 0 {
		return nil, fmt.Errorf(`%s didn't contain any transmit level data: "%v"`, section, record)
	}
	c := &bspb.BatteryStats_ControllerActivity{
		IdleTimeMsec: proto.Int64(idle),
		RxTimeMsec:   proto.Int64(rx),
		PowerMah:     proto.Int64(pwr),
	}
	for i, t := range rem {
		tm, err := strconv.Atoi(t)
		if err != nil {
			return nil, fmt.Errorf("%s contained invalid transmit value: %v", section, err)
		}
		c.Tx = append(c.Tx, &bspb.BatteryStats_ControllerActivity_TxLevel{
			Level:    proto.Int32(int32(i)),
			TimeMsec: proto.Int64(int64(tm)),
		})
	}
	return c, nil
}

// parseAndAccumulateControllerData parses any output that describes controller activity and
// adds the values to the existing proto. The input proto must not be nil.
//
// format: <idle_time>, <rx_time>, <power_ma_ms>, tx_time...
func parseAndAccumulateControllerData(pc checkinutil.Counter, section string, record []string, d *bspb.BatteryStats_ControllerActivity) error {
	if d == nil {
		return fmt.Errorf("provided nil ControllerActivity proto for %q", section)
	}
	c, err := parseControllerData(pc, section, record)
	if err != nil {
		return err
	}
	if len(d.Tx) != len(c.Tx) {
		return fmt.Errorf("%s had different number of transmit levels: %d vs %d", section, len(d.Tx), len(c.Tx))
	}
	d.IdleTimeMsec = proto.Int64(d.GetIdleTimeMsec() + c.GetIdleTimeMsec())
	d.RxTimeMsec = proto.Int64(d.GetRxTimeMsec() + c.GetRxTimeMsec())
	d.PowerMah = proto.Int64(d.GetPowerMah() + c.GetPowerMah())
	l := make(map[int32]int64)
	for _, t := range c.Tx {
		l[t.GetLevel()] = t.GetTimeMsec()
	}
	for _, t := range d.Tx {
		t.TimeMsec = proto.Int64(t.GetTimeMsec() + l[t.GetLevel()])
	}
	return nil
}

// parseSystemKernelWakelock parses "kwl" (KERNEL_WAKELOCK_DATA) in system
//
// format: 8,0,l,kwl,ipc000000b0_sensors.qcom,0,0
// wakelock name, time, count
func parseSystemKernelWakelock(record []string, system *bspb.BatteryStats_System) (string, []error) {
	w := &bspb.BatteryStats_System_KernelWakelock{}
	warn, plErrs := parseLine(kernelWakelockData, record, w)
	if len(plErrs) > 0 {
		return warn, plErrs
	}

	for _, kw := range system.KernelWakelock {
		if kw.GetName() == w.GetName() {
			kw.TimeMsec = proto.Float32(kw.GetTimeMsec() + w.GetTimeMsec())
			kw.Count = proto.Float32(kw.GetCount() + w.GetCount())
			return warn, nil
		}
	}

	// The kernel wakelock wasn't found in system's list of kernel wakelocks so add it as a new one.
	system.KernelWakelock = append(system.KernelWakelock, w)
	return warn, nil
}

// parseSystemMisc parses "m" (MISC_DATA) in System.
// format:
// 9,0,l,m,12469,0,20657343842,0,0,0,11258,0,0,3000,2,5000,1,2,1000,10000,10,15000,10,5000,3000 (reportVersion >= 16)
// 9,0,l,m,12469,0,20657343842,0,0,0,11258,0,0,3000,2,5000,1 (reportVersion >= 14)
// 9,0,l,m,12469,0,228195853,228195672,0,0,0,8889296,3246978,0,20657343842,0,0,0,11258,0,0 (8 <= reportVersion < 14)
// 8,0,l,m,47452,0,19133321,19133231,0,0,0,1863222,1605056,0,918161,0 (reportVersion < 8)
//
// screen on time, phone on time, [wifi on time, wifi running time,
// bluetooth on time, mobile rx total bytes, mobile tx total bytes,
// wifi rx total bytes, wifi tx total bytes, legacy input event count(always 0)]
// mobile radio active time, mobile radio active adjusted time
// low power mode enabled time, [# connectivity changes],
// [device idle mode enabled time, device idle mode enabled count,
// device idling time, device idling count], [mobile_active_count, mobile_active_unknown_time],
// [light idle mode enabled time, light idle mode enabled count, light idling time, light idling count,
// longest light idle mode enabled time, longest idle mode enabled time]
func parseSystemMisc(pc checkinutil.Counter, reportVersion int32, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetMisc() != nil {
		pc.Count("error-parse-system-misc-exist", 1)
		return "", []error{errors.New("misc field already exists")}
	}

	m := &bspb.BatteryStats_System_Misc{}
	// screen off time is not part of the line(calculated by subtracting screen
	// on time from battery real time. We put a dummy value in the input slice,
	// so we can still parse the other fields in the format specified by proto.
	record = append(record[:1], append([]string{"0"}, record[1:]...)...)

	var warn string
	var errs []error
	if reportVersion >= 14 {
		// Legacy data was removed from the line in version 14. Adding
		// dummy values so we can continue using predefined functions.
		record = append(record[:3], append([]string{"0", "0", "0", "0", "0", "0", "0"}, record[3:]...)...)
		warn, errs = parseLine(miscData, record, m)
	} else {
		warn, errs = parseLineWithSkip(miscData, record, m, []int{12} /* legacy input */)
	}

	if len(errs) == 0 {
		if reportVersion < 9 { // due to change in android client side
			m.FullWakelockTimeMsec = proto.Float32(m.GetFullWakelockTimeMsec() / 1e3)
			m.PartialWakelockTimeMsec = proto.Float32(m.GetPartialWakelockTimeMsec() / 1e3)
			m.MobileActiveTimeMsec = proto.Float32(m.GetMobileActiveTimeMsec() / 1e3)
			m.MobileActiveAdjustedTimeMsec = proto.Float32(m.GetMobileActiveAdjustedTimeMsec() / 1e3)
		}

		system.Misc = m
	}
	return warn, errs
}

// parseSystemBatteryDischarge parses "dc" (BATTERY_DISCHARGE_DATA) in System.
//
// record holds content from BatteryStats's batteryDischargeData.
//   e.g., 17,17,8,9
func parseSystemBatteryDischarge(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetBatteryDischarge() != nil {
		pc.Count("error-parse-system-battery-discharge-exist", 1)
		return "", []error{errors.New("battery discharge field already exists")}
	}
	b := &bspb.BatteryStats_System_BatteryDischarge{}
	warn, errs := parseLine(batteryDischargeData, record, b)
	if len(errs) == 0 {
		system.BatteryDischarge = b
	}
	return warn, errs
}

// parseSystemBatteryLevel parses "lv" (BATTERY_LEVEL_DATA) in System.
//
// record holds content from BatteryStats's batteryLevelData.
//   e.g., 100,83
func parseSystemBatteryLevel(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetBatteryLevel() != nil {
		pc.Count("error-parse-system-battery-level-exist", 1)
		return "", []error{errors.New("battery level field already exists")}
	}
	b := &bspb.BatteryStats_System_BatteryLevel{}
	warn, errs := parseLine(batteryLevelData, record, b)
	if len(errs) > 0 {
		return warn, errs
	}
	system.BatteryLevel = b
	return warn, nil
}

// parseGlobalBluetooth parses "gble" (GLOBAL_BLUETOOTH_DATA") into system, for report version < 17.
// format: 9,0,l,gble,15,16,17,18
// bluetooth_idle_time_msec, bluetooth_rx_time_msec, bluetooth_tx_time_msec, bluetooth_power_mah
func parseGlobalBluetooth(record []string, system *bspb.BatteryStats_System) (string, []error) {
	g := &bspb.BatteryStats_System_GlobalBluetooth{}
	warn, errs := parseLine(globalBluetoothControllerData, record, g)
	if len(errs) == 0 {
		system.GlobalBluetooth = g
	}
	return warn, errs
}

// parseGlobalNetwork parses "gn" (GLOBAL_NETWORK_DATA") into system.
// format: 0,0,1863222,1605056,0,0,5912,4257
func parseGlobalNetwork(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetGlobalNetwork() != nil {
		pc.Count("error-parse-system-global-network-exist", 1)
		return "", []error{errors.New("global network field already exists")}
	}
	n := &bspb.BatteryStats_System_GlobalNetwork{}
	warn, errs := parseLine(globalNetworkData, record, n)
	if len(errs) == 0 {
		system.GlobalNetwork = n
	}
	return warn, errs
}

// parseGlobalWifi parses "gwfl" (GLOBAL_WIFI_DATA") into system.
// format: 9,0,l,gwfl,9,10,11,12,13,14
// wifi_on_time_msec, wifi_running_time_msec, wifi_idle_time_msec, wifi_rx_time_msec, wifi_tx_time_msec, wifi_power_mah
func parseGlobalWifi(record []string, system *bspb.BatteryStats_System) (string, []error) {
	g := &bspb.BatteryStats_System_GlobalWifi{}
	warn, errs := parseLine(globalWifiData, record, g)
	if len(errs) == 0 {
		system.GlobalWifi = g
	}
	return warn, errs
}

// parseSystemScreenBrightness parses "br" (SCREEN_BRIGHTNESS_DATA) in System.
// format: 8,0,l,br,0,0,56369,0,0
// time spent from level 0 to 4
// (0: DARK, ..., 4:BRIGHT, see proto for details)
func parseSystemScreenBrightness(c checkinutil.Counter, record []string, system *bspb.BatteryStats_System) error {
	if len(system.GetScreenBrightness()) > 0 {
		c.Count("error-system-screen-brightness-exist", 1)
		return errors.New("screen brightness field already exists")
	}
	if len(record) != len(bspb.BatteryStats_System_ScreenBrightness_Name_name) {
		c.Count("error-system-screen-brightness-wrong-number-of-records", 1)
		return fmt.Errorf("wrong number of screen brightness fields. Got %d, want %d", len(record), len(bspb.BatteryStats_System_ScreenBrightness_Name_name))
	}
	for name, rawTimeMsec := range record {
		timeMsec, err := parseFloat32(rawTimeMsec)
		if err != nil {
			c.Count("error-parse-system-screen-brightness", 1)
			return err
		}
		system.ScreenBrightness = append(system.ScreenBrightness,
			&bspb.BatteryStats_System_ScreenBrightness{
				Name:     bspb.BatteryStats_System_ScreenBrightness_Name(name).Enum(),
				TimeMsec: proto.Float32(timeMsec),
			})
	}
	return nil
}

// parseSystemSignalScanningTime parses "sst" (SIGNAL_SCANNING_TIME_DATA) in system.
//
// format: 9,0,l,sst,9000
func parseSystemSignalScanningTime(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetSignalScanningTime() != nil {
		pc.Count("error-parse-system-signal-scanning-time-exist", 1)
		return "", []error{errors.New("signal scanning time field already exists")}
	}
	s := &bspb.BatteryStats_System_SignalScanningTime{}
	warn, errs := parseLine(signalScanningTimeData, record, s)
	if len(errs) == 0 {
		system.SignalScanningTime = s
	}
	return warn, errs
}

// parseSystemTimeCountPair parses categories that have time count pairs (in two separate lines in
// the checkin data).
//
// format: time ends with "t", count ends with "c"
// each has five values corresponds to five levels
// (0: NONE OR UNKNOW, ..., 4: GREAT, see proto for details)
// 8,0,l,sgt,2307235,8838772,22120797,18900758,0
// 8,0,l,sgc,19,60,85,38,1
//
// record holds content from one of the following sections in BatteryStats:
//   bluetoothStateTimeData, bluetoothStateCountData,
//   dataConnectionTimeData, dataConnectionCountData,
//   signalStrengthTimeData, signalStrengthCountData,
//   wifiSignalStrengthTimeData, wifiSignalStrengthCountData,
//   wifiStateTimeData, wifiStateCountData,
//   wifiSupplicantStateTimeData, wifiSupplicantStateCountData
//
// Note the sub-message (e.g., system.SignalStrength) is a repeated field whose i-th element
// corresponds to i-th element in record, and the index "i" here matches i-th Name (enum type) in
// each sub-message. Specifically, for any sub-message M (e.g., system.SignalStrength):
//   For any M.Name enum i:
//     system.M[i].Name = i
//     system.M[i].TimeMsec (or Count) = record[i]
//
// These sub-messages are not based on a single proto definition despite their similarity because
// there's no guarantee that the input BatteryStats data (csv file) from Android client will stick
// to this similarity. (e.g., a new field may be added only for wifiStateTimeData, which would
// incur data migration if a single proto were used). This function just aims to reuse some common
// code for such sub-messages.
func parseSystemTimeCountPair(c checkinutil.Counter, section string, record []string, system *bspb.BatteryStats_System) (string, error) {
	dataLen := len(record)
	var currentLen, expectedLen int
	switch section {
	case signalStrengthTimeData, signalStrengthCountData:
		currentLen = len(system.SignalStrength)
		expectedLen = len(bspb.BatteryStats_System_SignalStrength_Name_name)
	case dataConnectionTimeData, dataConnectionCountData:
		currentLen = len(system.DataConnection)
		expectedLen = len(bspb.BatteryStats_System_DataConnection_Name_name)
	case wifiStateTimeData, wifiStateCountData:
		currentLen = len(system.WifiState)
		expectedLen = len(bspb.BatteryStats_System_WifiState_Name_name)
	case bluetoothStateTimeData, bluetoothStateCountData:
		currentLen = len(system.BluetoothState)
		expectedLen = len(bspb.BatteryStats_System_BluetoothState_Name_name)
	case wifiSupplStateTimeData, wifiSupplStateCountData:
		currentLen = len(system.WifiSupplicantState)
		expectedLen = len(bspb.BatteryStats_System_WifiSupplicantState_Name_name)
	case wifiSignalStrengthTimeData, wifiSignalStrengthCountData:
		currentLen = len(system.WifiSignalStrength)
		expectedLen = len(bspb.BatteryStats_System_WifiSignalStrength_Name_name)
	default:
		return "", fmt.Errorf("parseSystemTimeCountPair encountered unknown section: %s", section) // Shouldn't happen since we specifically filter which sections get sent to this function.
	}

	warning := ""
	if dataLen < expectedLen {
		c.Count("error-system-"+section+"-too-few-records", 1)
		return "", fmt.Errorf("%s doesn't contain enough records", section)
	} else if dataLen > expectedLen {
		c.Count("error-system-"+section+"-too-many-records", 1)
		warning = fmt.Sprintf("%s has %d additional field(s) that are not captured.", section, dataLen-expectedLen)
	}

	if currentLen == 0 { // No proto exists. Create a new proto and fill the field.
		for name, rawValue := range record {
			value, err := parseFloat32(rawValue)
			if err != nil {
				return warning, err
			}
			switch section {
			case signalStrengthTimeData:
				system.SignalStrength = append(system.SignalStrength,
					&bspb.BatteryStats_System_SignalStrength{
						Name:     bspb.BatteryStats_System_SignalStrength_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case signalStrengthCountData:
				system.SignalStrength = append(system.SignalStrength,
					&bspb.BatteryStats_System_SignalStrength{
						Name:  bspb.BatteryStats_System_SignalStrength_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case dataConnectionTimeData:
				system.DataConnection = append(system.DataConnection,
					&bspb.BatteryStats_System_DataConnection{
						Name:     bspb.BatteryStats_System_DataConnection_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case dataConnectionCountData:
				system.DataConnection = append(system.DataConnection,
					&bspb.BatteryStats_System_DataConnection{
						Name:  bspb.BatteryStats_System_DataConnection_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case wifiStateTimeData:
				system.WifiState = append(system.WifiState,
					&bspb.BatteryStats_System_WifiState{
						Name:     bspb.BatteryStats_System_WifiState_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case wifiStateCountData:
				system.WifiState = append(system.WifiState,
					&bspb.BatteryStats_System_WifiState{
						Name:  bspb.BatteryStats_System_WifiState_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case bluetoothStateTimeData:
				system.BluetoothState = append(system.BluetoothState,
					&bspb.BatteryStats_System_BluetoothState{
						Name:     bspb.BatteryStats_System_BluetoothState_Name(name).Enum(),
						TimeMsec: proto.Float32(value),
					})
			case bluetoothStateCountData:
				system.BluetoothState = append(system.BluetoothState,
					&bspb.BatteryStats_System_BluetoothState{
						Name:  bspb.BatteryStats_System_BluetoothState_Name(name).Enum(),
						Count: proto.Float32(value),
					})
			case wifiSignalStrengthTimeData:
				system.WifiSignalStrength = append(system.WifiSignalStrength, &bspb.BatteryStats_System_WifiSignalStrength{
					Name:     bspb.BatteryStats_System_WifiSignalStrength_Name(name).Enum(),
					TimeMsec: proto.Float32(value),
				})
			case wifiSignalStrengthCountData:
				system.WifiSignalStrength = append(system.WifiSignalStrength, &bspb.BatteryStats_System_WifiSignalStrength{
					Name:  bspb.BatteryStats_System_WifiSignalStrength_Name(name).Enum(),
					Count: proto.Float32(value),
				})
			case wifiSupplStateTimeData:
				system.WifiSupplicantState = append(system.WifiSupplicantState, &bspb.BatteryStats_System_WifiSupplicantState{
					Name:     bspb.BatteryStats_System_WifiSupplicantState_Name(name).Enum(),
					TimeMsec: proto.Float32(value),
				})
			case wifiSupplStateCountData:
				system.WifiSupplicantState = append(system.WifiSupplicantState, &bspb.BatteryStats_System_WifiSupplicantState{
					Name:  bspb.BatteryStats_System_WifiSupplicantState_Name(name).Enum(),
					Count: proto.Float32(value),
				})
			default:
				return warning, fmt.Errorf("parseSystemTimeCountPair encountered unknown section: %s", section) // Shouldn't happen, explained above.
			}
		}
	} else if currentLen == len(record) { // The proto exists. Just fill the field.
		for name, rawValue := range record {
			value, err := parseFloat32(rawValue)
			if err != nil {
				return warning, err
			}
			var prev *float32
			switch section {
			case signalStrengthTimeData:
				prev = system.SignalStrength[name].TimeMsec
				system.SignalStrength[name].TimeMsec = proto.Float32(value)
			case signalStrengthCountData:
				prev = system.SignalStrength[name].Count
				system.SignalStrength[name].Count = proto.Float32(value)
			case dataConnectionTimeData:
				prev = system.DataConnection[name].TimeMsec
				system.DataConnection[name].TimeMsec = proto.Float32(value)
			case dataConnectionCountData:
				prev = system.DataConnection[name].Count
				system.DataConnection[name].Count = proto.Float32(value)
			case wifiStateTimeData:
				prev = system.WifiState[name].TimeMsec
				system.WifiState[name].TimeMsec = proto.Float32(value)
			case wifiStateCountData:
				prev = system.WifiState[name].Count
				system.WifiState[name].Count = proto.Float32(value)
			case bluetoothStateTimeData:
				prev = system.BluetoothState[name].TimeMsec
				system.BluetoothState[name].TimeMsec = proto.Float32(value)
			case bluetoothStateCountData:
				prev = system.BluetoothState[name].Count
				system.BluetoothState[name].Count = proto.Float32(value)
			case wifiSupplStateTimeData:
				prev = system.WifiSupplicantState[name].TimeMsec
				system.WifiSupplicantState[name].TimeMsec = proto.Float32(value)
			case wifiSupplStateCountData:
				prev = system.WifiSupplicantState[name].Count
				system.WifiSupplicantState[name].Count = proto.Float32(value)
			case wifiSignalStrengthTimeData:
				prev = system.WifiSignalStrength[name].TimeMsec
				system.WifiSignalStrength[name].TimeMsec = proto.Float32(value)
			case wifiSignalStrengthCountData:
				prev = system.WifiSignalStrength[name].Count
				system.WifiSignalStrength[name].Count = proto.Float32(value)
			default:
				return warning, fmt.Errorf("parseSystemTimeCountPair encountered unknown section: %s", section)
			}
			if prev != nil {
				c.Count("error-system-"+section+"-exist", 1)
				return warning, fmt.Errorf("%s section already exists", section)
			}
		}
	} else {
		return warning, fmt.Errorf("inconsistent number of fields in %s", section) // Shouldn't happen
	}
	return warning, nil
}

// parseSystemWakeupReason parses "wr" (WAKEUP_REASON_DATA) in System. These are low-level messages and don't contain PII.
//
// record holds content from BatteryStats's wakeupReasonData.
//   e.g., "289:bcmsdh_sdmmc:200:qcom,mpm:240:msmgpio",308628 (reportVersion < 11)
//   e.g., "289:bcmsdh_sdmmc:200:qcom,mpm:240:msmgpio",308628,399 (reportVersion >= 11)
func parseSystemWakeupReason(record []string, system *bspb.BatteryStats_System) (string, []error) {
	// This time is the amount of time we saw the CPU awake from when we received
	// a wake reason until we had another reason for it to be awake (someone
	// acquiring a user space wake lock or another sleep/wake happening).
	// 8,0,l,wr,"200:qcom,smd-rpm:222:fc4cf000.qcom,spmi",760
	wr := &bspb.BatteryStats_System_WakeupReason{}
	warn, errs := parseLine(wakeupReasonData, record, wr)
	if len(errs) > 0 {
		return warn, errs
	}
	system.WakeupReason = append(system.WakeupReason, wr)
	return warn, nil
}

// parseSystemBattery parses "bt" (BATTERY_DATA) in System.
//
// format:
//   7,0,l,bt,N/A,4946,4946,25430,25430 (reportVersion < 8)
//   9,0,l,bt,0,19447364,2268899,19466586,2288120,1411399763148,19399912,2221446 (reportVersion >= 8)
func parseSystemBattery(pc checkinutil.Counter, record []string, system *bspb.BatteryStats_System) (string, []error) {
	if system.GetBattery() != nil {
		pc.Count("error-parse-system-battery-exist", 1)
		return "", []error{errors.New("system battery field already exists")}
	}
	if record[0] == "N/A" {
		// Batterystats pipeline defaults startCount to 0 in the case of a N/A
		record = append([]string{"0"}, record[1:]...)
	}
	b := &bspb.BatteryStats_System_Battery{}
	warn, errs := parseLine(batteryData, record, b)
	if len(errs) == 0 {
		system.Battery = b
	}
	return warn, errs
}

// parseSlice wraps sliceparse.Consume(value, outputs...), increasing pc if it returns an error.
func parseSlice(pc checkinutil.Counter, name string, value []string, outputs ...interface{}) ([]string, error) {
	remaining, err := sliceparse.Consume(value, outputs...)
	if err != nil {
		pc.Count("error-parse-slice-"+name, 1)
	}
	return remaining, err
}

// parseFloat32 parses an individual 32-bit float.
func parseFloat32(s string) (float32, error) {
	f64, err := strconv.ParseFloat(s, 32)
	return float32(f64), err
}

func parseValue(src string, dstV reflect.Value) error {
	var err error
	srcSlice := []string{src}
	switch dstV.Kind() {
	case reflect.Float32:
		var val float32
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.Float64:
		var val float64
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.Int32:
		var val int32
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.Int64:
		var val int64
		_, err = sliceparse.Consume(srcSlice, &val)
		dstV.Set(reflect.ValueOf(val))
	case reflect.String:
		dstV.Set(reflect.ValueOf(src))
	default:
		return fmt.Errorf("parse error: type %s not supported", dstV.Kind().String())
	}
	return err
}

// parseAndAccumulate is a generic parsing function that parses a string slice into a proto,
// accumulating corresponding values in the input proto. The input proto must not be nil.
// This function cannot deal with nested protos.
func parseAndAccumulate(section string, record []string, p proto.Message) (string, []error) {
	if p == nil {
		return "", []error{fmt.Errorf("nil proto parsed for %s section", section)}
	}
	acc := reflect.ValueOf(p)

	// parse
	rec := reflect.New(acc.Type().Elem())
	warn, errs := parseLine(section, record, rec.Interface().(proto.Message))
	if len(errs) > 0 {
		return warn, errs
	}

	// and accumulate
	for i := 0; i < rec.Elem().NumField()-1; i++ {
		// pointer to field i. Golang requires the temporary variables in order for the rest of this processing to work.
		accPtr, recPtr := acc.Elem().Field(i), rec.Elem().Field(i)
		switch recPtr.Elem().Kind() {
		case reflect.Float32, reflect.Float64:
			if accPtr.IsNil() {
				accPtr.Set(recPtr)
			} else {
				accPtr.Elem().SetFloat(accPtr.Elem().Float() + recPtr.Elem().Float())
			}
		}
	}

	return warn, nil
}

// parseLineWithSkip skips string with specified index in record then uses parseLine
// to parse the remaining strings.
func parseLineWithSkip(section string, record []string, p proto.Message, skip []int) (string, []error) {
	sort.Ints(skip)
	next, nextSkip := 0, 0 // use next to traverse record, nextSkip to traverse skip.
	for i, s := range record {
		if nextSkip != -1 && i == skip[nextSkip] {
			nextSkip++
			if nextSkip == len(skip) {
				// stop skipping
				nextSkip = -1
			}
		} else {
			record[next] = s
			next++
		}
	}
	// Only need to pass the elements that were not skipped.
	return parseLine(section, record[:next], p)
}

// parseLine is a generic parsing function that parses a string slice into a proto.
// The proto should only have fields of type float32, float64, int32, int64 or string.
// The function determines the type a string should be parsed into by the type
// and order specified in the proto.
func parseLine(section string, record []string, p proto.Message) (string, []error) {
	var warn string
	var errs []error
	pV := reflect.ValueOf(p)
	// Because of backwards compatibility, there will be times when the proto
	// will have more fields than record can provide, so just fill up as many
	// fields as possible.
	num := len(record)
	if len(record) > pV.Elem().NumField()-1 {
		num = pV.Elem().NumField() - 1
		warn = fmt.Sprintf("The underlying format for %s has %d additional field(s) that are not captured.", section, len(record)-num)
	}

	for i := 0; i < num; i++ {
		valPtrV := reflect.New(pV.Elem().Field(i).Type().Elem())
		if err := parseValue(record[i], valPtrV.Elem()); err == nil {
			pV.Elem().Field(i).Set(valPtrV)
		} else {
			errs = append(errs, fmt.Errorf("error parsing %s: %v", section, err))
		}
	}
	return warn, errs
}
