1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.desktopmode.education.data 18 19 import android.content.Context 20 import android.util.Log 21 import androidx.datastore.core.CorruptionException 22 import androidx.datastore.core.DataStore 23 import androidx.datastore.core.DataStoreFactory 24 import androidx.datastore.core.Serializer 25 import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 26 import androidx.datastore.dataStoreFile 27 import com.android.framework.protobuf.InvalidProtocolBufferException 28 import com.android.internal.annotations.VisibleForTesting 29 import java.io.IOException 30 import java.io.InputStream 31 import java.io.OutputStream 32 import java.time.Duration 33 import kotlinx.coroutines.flow.Flow 34 import kotlinx.coroutines.flow.catch 35 import kotlinx.coroutines.flow.first 36 37 /** 38 * Manages interactions with the App Handle education datastore. 39 * 40 * This class provides a layer of abstraction between the UI/business logic and the underlying 41 * DataStore. 42 */ 43 class AppHandleEducationDatastoreRepository 44 @VisibleForTesting 45 constructor(private val dataStore: DataStore<WindowingEducationProto>) { 46 constructor( 47 context: Context 48 ) : this( 49 DataStoreFactory.create( 50 serializer = WindowingEducationProtoSerializer, 51 produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }, 52 corruptionHandler = 53 ReplaceFileCorruptionHandler( 54 produceNewData = { WindowingEducationProto.getDefaultInstance() } 55 ), 56 ) 57 ) 58 59 /** Provides dataStore.data flow and handles exceptions thrown during collection */ 60 val dataStoreFlow: Flow<WindowingEducationProto> = 61 dataStore.data.catch { exception -> 62 // dataStore.data throws an IOException when an error is encountered when reading data 63 if (exception is IOException) { 64 Log.e( 65 TAG, 66 "Error in reading app handle education related data from datastore, data is " + 67 "stored in a file named $APP_HANDLE_EDUCATION_DATASTORE_FILEPATH", 68 exception, 69 ) 70 } else { 71 throw exception 72 } 73 } 74 75 /** 76 * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the 77 * DataStore is empty or there's an error reading, it returns the default value of Proto. 78 */ 79 suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() 80 81 /** 82 * Updates [WindowingEducationProto.appHandleHintViewedTimestampMillis_] field in datastore with 83 * current timestamp if [isViewed] is true, if not then clears the field. 84 */ 85 suspend fun updateAppHandleHintViewedTimestampMillis(isViewed: Boolean) { 86 dataStore.updateData { preferences -> 87 if (isViewed) { 88 preferences 89 .toBuilder() 90 .setAppHandleHintViewedTimestampMillis(System.currentTimeMillis()) 91 .build() 92 } else { 93 preferences.toBuilder().clearAppHandleHintViewedTimestampMillis().build() 94 } 95 } 96 } 97 98 /** 99 * Updates [WindowingEducationProto.enterDesktopModeHintViewedTimestampMillis_] field in 100 * datastore with current timestamp if [isViewed] is true, if not then clears the field. 101 */ 102 suspend fun updateEnterDesktopModeHintViewedTimestampMillis(isViewed: Boolean) { 103 dataStore.updateData { preferences -> 104 if (isViewed) { 105 preferences 106 .toBuilder() 107 .setEnterDesktopModeHintViewedTimestampMillis(System.currentTimeMillis()) 108 .build() 109 } else { 110 preferences.toBuilder().clearEnterDesktopModeHintViewedTimestampMillis().build() 111 } 112 } 113 } 114 115 /** 116 * Updates [WindowingEducationProto.exitDesktopModeHintViewedTimestampMillis_] field in 117 * datastore with current timestamp if [isViewed] is true, if not then clears the field. 118 */ 119 suspend fun updateExitDesktopModeHintViewedTimestampMillis(isViewed: Boolean) { 120 dataStore.updateData { preferences -> 121 if (isViewed) { 122 preferences 123 .toBuilder() 124 .setExitDesktopModeHintViewedTimestampMillis(System.currentTimeMillis()) 125 .build() 126 } else { 127 preferences.toBuilder().clearExitDesktopModeHintViewedTimestampMillis().build() 128 } 129 } 130 } 131 132 /** 133 * Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field in datastore with 134 * current timestamp if [isViewed] is true, if not then clears the field. 135 */ 136 suspend fun updateAppHandleHintUsedTimestampMillis(isViewed: Boolean) { 137 dataStore.updateData { preferences -> 138 if (isViewed) { 139 preferences 140 .toBuilder() 141 .setAppHandleHintUsedTimestampMillis(System.currentTimeMillis()) 142 .build() 143 } else { 144 preferences.toBuilder().clearAppHandleHintUsedTimestampMillis().build() 145 } 146 } 147 } 148 149 /** 150 * Updates [AppHandleEducation.appUsageStats] and 151 * [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with 152 * [appUsageStats] and [appUsageStatsLastUpdateTimestamp]. 153 */ 154 suspend fun updateAppUsageStats( 155 appUsageStats: Map<String, Int>, 156 appUsageStatsLastUpdateTimestamp: Duration, 157 ) { 158 val currentAppHandleProto = windowingEducationProto().appHandleEducation.toBuilder() 159 currentAppHandleProto 160 .putAllAppUsageStats(appUsageStats) 161 .setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestamp.toMillis()) 162 dataStore.updateData { preferences: WindowingEducationProto -> 163 preferences.toBuilder().setAppHandleEducation(currentAppHandleProto).build() 164 } 165 } 166 167 companion object { 168 private const val TAG = "AppHandleEducationDatastoreRepository" 169 private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb" 170 171 object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> { 172 173 override val defaultValue: WindowingEducationProto = 174 WindowingEducationProto.getDefaultInstance() 175 176 override suspend fun readFrom(input: InputStream): WindowingEducationProto = 177 try { 178 WindowingEducationProto.parseFrom(input) 179 } catch (exception: InvalidProtocolBufferException) { 180 throw CorruptionException("Cannot read proto.", exception) 181 } 182 183 override suspend fun writeTo( 184 windowingProto: WindowingEducationProto, 185 output: OutputStream, 186 ) = windowingProto.writeTo(output) 187 } 188 } 189 } 190