Implement tasks that CarStorageMonitoringService should perform upon detecting a wear change:

  - log on adverse wear level (pre EOL > normal, wear >= 80%)
  - launch an OEM-specific activity configured via CarService overlay, if any

Bug: 32512551
Test: manual
Change-Id: I05d22a5110adb021380b1ff6ff5405d4051c0a72
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 9984726..6908eeb 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -31,6 +31,7 @@
 
 # This is for testing
 PRODUCT_PACKAGES += \
+    DefaultStorageMonitoringCompanionApp \
     EmbeddedKitchenSinkApp \
     VmsPublisherClientSample \
     VmsSubscriberClientSample \
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 68996d1..f5cf0f5 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -90,4 +90,11 @@
          about the total running time of the head-unit. A shutdown or reboot of the head-unit
           will always cause a flush of the uptime information, regardless of this setting. -->
     <integer name="uptimeHoursIntervalBetweenUptimeDataWrite">5</integer>
+
+    <!-- The name of an activity to be launched by CarService whenever it detects a change in the
+         level of wear of the flash storage. Value must either be an empty string, which means that
+         no activity shall be launched, or must be in the format of a flattened ComponentName and
+         reference a valid activity. It is strongly recommended that the chosen activity be
+         protected with the android.car.permission.STORAGE_MONITORING permission. -->
+    <string name="activityHandlerForFlashWearChanges">com.google.android.car.defaultstoragemonitoringcompanionapp/.MainActivity</string>
 </resources>
diff --git a/service/src/com/android/car/CarStorageMonitoringService.java b/service/src/com/android/car/CarStorageMonitoringService.java
index b4542ac..d933455 100644
--- a/service/src/com/android/car/CarStorageMonitoringService.java
+++ b/service/src/com/android/car/CarStorageMonitoringService.java
@@ -20,6 +20,8 @@
 import android.car.storagemonitoring.ICarStorageMonitoring;
 import android.car.storagemonitoring.WearEstimate;
 import android.car.storagemonitoring.WearEstimateChange;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.util.JsonWriter;
@@ -37,6 +39,7 @@
 import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
@@ -45,6 +48,7 @@
 public class CarStorageMonitoringService extends ICarStorageMonitoring.Stub
         implements CarServiceBase {
     private static final String TAG = CarLog.TAG_STORAGE;
+    private static final int MIN_WEAR_ESTIMATE_OF_CONCERN = 80;
 
     static final String UPTIME_TRACKER_FILENAME = "service_uptime";
     static final String WEAR_INFO_FILENAME = "wear_info";
@@ -151,6 +155,33 @@
             mSystemInterface);
     }
 
+    private void launchWearChangeActivity() {
+        final String activityPath = mContext.getResources().getString(
+            R.string.activityHandlerForFlashWearChanges);
+        if (activityPath.isEmpty()) return;
+        try {
+            final ComponentName activityComponent =
+                Objects.requireNonNull(ComponentName.unflattenFromString(activityPath));
+            Intent intent = new Intent();
+            intent.setComponent(activityComponent);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivity(intent);
+        } catch (ActivityNotFoundException | NullPointerException e) {
+            Log.e(TAG,
+                "value of activityHandlerForFlashWearChanges invalid non-empty string " +
+                    activityPath, e);
+        }
+    }
+
+    private static void logOnAdverseWearLevel(WearInformation wearInformation) {
+        if (wearInformation.preEolInfo > WearInformation.PRE_EOL_INFO_NORMAL ||
+            Math.max(wearInformation.lifetimeEstimateA,
+                wearInformation.lifetimeEstimateB) >= MIN_WEAR_ESTIMATE_OF_CONCERN) {
+            Log.w(TAG, "flash storage reached wear a level that requires attention: "
+                    + wearInformation);
+        }
+    }
+
     private synchronized void doInitServiceIfNeeded() {
         if (mInitialized) return;
 
@@ -160,7 +191,8 @@
 
         // TODO(egranata): can this be done lazily?
         final WearHistory wearHistory = loadWearHistory();
-        if (addEventIfNeeded(wearHistory)) {
+        final boolean didWearChangeHappen = addEventIfNeeded(wearHistory);
+        if (didWearChangeHappen) {
             storeWearHistory(wearHistory);
         }
         Log.d(TAG, "wear history being tracked is " + wearHistory);
@@ -170,6 +202,12 @@
 
         mOnShutdownReboot.addAction((Context ctx, Intent intent) -> release());
 
+        mWearInformation.ifPresent(CarStorageMonitoringService::logOnAdverseWearLevel);
+
+        if (didWearChangeHappen) {
+            launchWearChangeActivity();
+        }
+
         Log.i(TAG, "CarStorageMonitoringService is up");
 
         mInitialized = true;
diff --git a/tests/DefaultStorageMonitoringCompanionApp/Android.mk b/tests/DefaultStorageMonitoringCompanionApp/Android.mk
new file mode 100644
index 0000000..8eb2a55
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/Android.mk
@@ -0,0 +1,39 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := DefaultStorageMonitoringCompanionApp
+
+LOCAL_AAPT_FLAGS := --auto-add-overlay
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_DEX_PREOPT := false
+
+LOCAL_JAVA_LIBRARIES += android.car
+
+include $(BUILD_PACKAGE)
diff --git a/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml b/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml
new file mode 100644
index 0000000..45380ce
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.car.defaultstoragemonitoringcompanionapp">
+
+  <uses-permission android:name="android.car.permission.STORAGE_MONITORING" />
+
+  <application
+      android:allowBackup="true"
+      android:icon="@mipmap/ic_launcher"
+      android:label="@string/app_name"
+      android:roundIcon="@mipmap/ic_launcher_round"
+      android:supportsRtl="true"
+      android:theme="@style/AppTheme">
+    <activity
+        android:name=".MainActivity"
+        android:exported="true"
+        android:permission="android.car.permission.STORAGE_MONITORING">
+    </activity>
+  </application>
+
+</manifest>
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/layout/activity_main.xml b/tests/DefaultStorageMonitoringCompanionApp/res/layout/activity_main.xml
new file mode 100644
index 0000000..017ebcc
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/notification"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:minLines="10"
+        android:textSize="24dp"
+        android:textColor="#ff0000"
+        android:text="Gathering data..."/>
+
+</RelativeLayout>
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-hdpi/ic_launcher.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-hdpi/ic_launcher_round.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9a078e3
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-mdpi/ic_launcher.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-mdpi/ic_launcher_round.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..efc028a
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xhdpi/ic_launcher.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xhdpi/ic_launcher_round.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..3af2608
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxhdpi/ic_launcher.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxhdpi/ic_launcher_round.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9bec2e6
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxxhdpi/ic_launcher.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxxhdpi/ic_launcher_round.png b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..34947cd
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/values/colors.xml b/tests/DefaultStorageMonitoringCompanionApp/res/values/colors.xml
new file mode 100644
index 0000000..5a077b3
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="colorPrimary">#3F51B5</color>
+  <color name="colorPrimaryDark">#303F9F</color>
+  <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/values/strings.xml b/tests/DefaultStorageMonitoringCompanionApp/res/values/strings.xml
new file mode 100644
index 0000000..8c0a9bb
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+  <string name="app_name">Flash Storage Wear Notification</string>
+</resources>
diff --git a/tests/DefaultStorageMonitoringCompanionApp/res/values/styles.xml b/tests/DefaultStorageMonitoringCompanionApp/res/values/styles.xml
new file mode 100644
index 0000000..a7a0615
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources>
+
+  <!-- Base application theme. -->
+  <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
+    <!-- Customize your theme here. -->
+  </style>
+
+</resources>
diff --git a/tests/DefaultStorageMonitoringCompanionApp/src/com/google/android/car/defaultstoragemonitoringcompanionapp/MainActivity.java b/tests/DefaultStorageMonitoringCompanionApp/src/com/google/android/car/defaultstoragemonitoringcompanionapp/MainActivity.java
new file mode 100644
index 0000000..bcec38b
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/src/com/google/android/car/defaultstoragemonitoringcompanionapp/MainActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.car.defaultstoragemonitoringcompanionapp;
+
+import android.app.Activity;
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.storagemonitoring.CarStorageMonitoringManager;
+import android.car.storagemonitoring.WearEstimate;
+import android.car.storagemonitoring.WearEstimateChange;
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+import android.widget.TextView;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.List;
+import java.util.Locale;
+
+public class MainActivity extends Activity {
+    private static final boolean DEBUG = false;
+    private static final String TAG = MainActivity.class.getSimpleName();
+
+    private final ServiceConnection mCarConnectionListener = new ServiceConnection() {
+        private String wearLevelToString(int wearLevel) {
+            if (wearLevel == WearEstimate.UNKNOWN) return "unknown";
+            return wearLevel + "%";
+        }
+
+        private String durationPairToString(long value1, String name1,
+            long value2, String name2) {
+            if (value1 > 0) {
+                String s = value1 + " " + name1;
+                if (value2 > 0) {
+                    s += " and " + value2 + " " + name2;
+                }
+                return s;
+            }
+            return null;
+        }
+
+        private String durationToString(Duration duration) {
+            final long days = duration.toDays();
+            duration = duration.minusDays(days);
+            final long hours = duration.toHours();
+            duration = duration.minusHours(hours);
+            final long minutes = duration.toMinutes();
+
+            // for a week or more, just return days
+            if (days >= 7) {
+                return days + " days";
+            }
+
+            // otherwise try a few pairs of units
+            final String daysHours = durationPairToString(days, "days", hours, "hours");
+            if (daysHours != null) return daysHours;
+            final String hoursMinutes = durationPairToString(hours, "hours", minutes, "minutes");
+            if (hoursMinutes != null) return hoursMinutes;
+
+            // either minutes, or less than a minute
+            if (minutes > 0) {
+                return minutes + " minutes";
+            } else {
+                return "less than a minute";
+            }
+        }
+
+        private String wearChangeToString(WearEstimateChange wearEstimateChange) {
+            final int oldLevel = Math.max(wearEstimateChange.oldEstimate.typeA,
+                wearEstimateChange.oldEstimate.typeB);
+            final int newLevel = Math.max(wearEstimateChange.newEstimate.typeA,
+                wearEstimateChange.newEstimate.typeB);
+
+            final String oldLevelString = wearLevelToString(oldLevel);
+            final String newLevelString = wearLevelToString(newLevel);
+
+            final DateTimeFormatter formatter =
+                DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(
+                    ZoneId.systemDefault()).withLocale(Locale.getDefault());
+
+            final String wallClockTimeAtChange = formatter.format(wearEstimateChange.dateAtChange);
+            final String uptimeAtChange = durationToString(
+                Duration.ofMillis(wearEstimateChange.uptimeAtChange));
+
+            return String.format(
+                "Wear level went from %s to %s.\nThe vehicle has been running for %s.\nWall clock time: %s",
+                oldLevelString,
+                newLevelString,
+                uptimeAtChange,
+                wallClockTimeAtChange);
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.d(TAG, "Connected to " + name.flattenToString());
+
+            try {
+                CarStorageMonitoringManager storageMonitoringManager =
+                    (CarStorageMonitoringManager) mCar.getCarManager(
+                        Car.STORAGE_MONITORING_SERVICE);
+                Log.d(TAG, "Acquired a CarStorageMonitoringManager " + storageMonitoringManager);
+                List<WearEstimateChange> wearEstimateChanges = storageMonitoringManager
+                    .getWearEstimateHistory();
+                if (wearEstimateChanges.isEmpty()) {
+                    finish();
+                }
+
+                WearEstimateChange currentChange = wearEstimateChanges
+                    .get(wearEstimateChanges.size() - 1);
+
+                if (!DEBUG && currentChange.isAcceptableDegradation) {
+                    finish();
+                }
+
+                    mNotificationTextView.setText(wearChangeToString(currentChange));
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Failed to get a connection", e);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.d(TAG, "Disconnected from " + name.flattenToString());
+
+            mCar = null;
+        }
+    };
+
+    private final Handler mHandler = new Handler();
+
+    private Car mCar = null;
+    private TextView mNotificationTextView = null;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        mNotificationTextView = findViewById(R.id.notification);
+
+        mCar = Car.createCar(this, mCarConnectionListener);
+        mCar.connect();
+
+        mHandler.postDelayed(this::finish, 30000);
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mCar != null) {
+            mCar.disconnect();
+        }
+        super.onDestroy();
+    }
+}