• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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