1 /*
2  * Copyright (C) 2019 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 package android.view.textclassifier;
17 
18 import android.annotation.Nullable;
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.provider.Settings;
23 import android.text.TextUtils;
24 import android.util.Base64;
25 import android.util.KeyValueListParser;
26 
27 import com.android.internal.annotations.GuardedBy;
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.util.Preconditions;
30 
31 import java.lang.ref.WeakReference;
32 import java.util.Objects;
33 import java.util.function.Supplier;
34 
35 /**
36  * Parses the {@link Settings.Global#TEXT_CLASSIFIER_ACTION_MODEL_PARAMS} flag.
37  *
38  * @hide
39  */
40 public final class ActionsModelParamsSupplier implements
41         Supplier<ActionsModelParamsSupplier.ActionsModelParams> {
42     private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
43 
44     @VisibleForTesting
45     static final String KEY_REQUIRED_MODEL_VERSION = "required_model_version";
46     @VisibleForTesting
47     static final String KEY_REQUIRED_LOCALES = "required_locales";
48     @VisibleForTesting
49     static final String KEY_SERIALIZED_PRECONDITIONS = "serialized_preconditions";
50 
51     private final Context mAppContext;
52     private final SettingsObserver mSettingsObserver;
53 
54     private final Object mLock = new Object();
55     private final Runnable mOnChangedListener;
56     @Nullable
57     @GuardedBy("mLock")
58     private ActionsModelParams mActionsModelParams;
59     @GuardedBy("mLock")
60     private boolean mParsed = true;
61 
ActionsModelParamsSupplier(Context context, @Nullable Runnable onChangedListener)62     public ActionsModelParamsSupplier(Context context, @Nullable Runnable onChangedListener) {
63         final Context appContext = Preconditions.checkNotNull(context).getApplicationContext();
64         // Some contexts don't have an app context.
65         mAppContext = appContext != null ? appContext : context;
66         mOnChangedListener = onChangedListener == null ? () -> {} : onChangedListener;
67         mSettingsObserver = new SettingsObserver(mAppContext, () -> {
68             synchronized (mLock) {
69                 Log.v(TAG, "Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS is updated");
70                 mParsed = true;
71                 mOnChangedListener.run();
72             }
73         });
74     }
75 
76     /**
77      * Returns the parsed actions params or {@link ActionsModelParams#INVALID} if the value is
78      * invalid.
79      */
80     @Override
get()81     public ActionsModelParams get() {
82         synchronized (mLock) {
83             if (mParsed) {
84                 mActionsModelParams = parse(mAppContext.getContentResolver());
85                 mParsed = false;
86             }
87         }
88         return mActionsModelParams;
89     }
90 
parse(ContentResolver contentResolver)91     private ActionsModelParams parse(ContentResolver contentResolver) {
92         String settingStr = Settings.Global.getString(contentResolver,
93                 Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS);
94         if (TextUtils.isEmpty(settingStr)) {
95             return ActionsModelParams.INVALID;
96         }
97         try {
98             KeyValueListParser keyValueListParser = new KeyValueListParser(',');
99             keyValueListParser.setString(settingStr);
100             int version = keyValueListParser.getInt(KEY_REQUIRED_MODEL_VERSION, -1);
101             if (version == -1) {
102                 Log.w(TAG, "ActionsModelParams.Parse, invalid model version");
103                 return ActionsModelParams.INVALID;
104             }
105             String locales = keyValueListParser.getString(KEY_REQUIRED_LOCALES, null);
106             if (locales == null) {
107                 Log.w(TAG, "ActionsModelParams.Parse, invalid locales");
108                 return ActionsModelParams.INVALID;
109             }
110             String serializedPreconditionsStr =
111                     keyValueListParser.getString(KEY_SERIALIZED_PRECONDITIONS, null);
112             if (serializedPreconditionsStr == null) {
113                 Log.w(TAG, "ActionsModelParams.Parse, invalid preconditions");
114                 return ActionsModelParams.INVALID;
115             }
116             byte[] serializedPreconditions =
117                     Base64.decode(serializedPreconditionsStr, Base64.NO_WRAP);
118             return new ActionsModelParams(version, locales, serializedPreconditions);
119         } catch (Throwable t) {
120             Log.e(TAG, "Invalid TEXT_CLASSIFIER_ACTION_MODEL_PARAMS, ignore", t);
121         }
122         return ActionsModelParams.INVALID;
123     }
124 
125     @Override
finalize()126     protected void finalize() throws Throwable {
127         try {
128             mAppContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
129         } finally {
130             super.finalize();
131         }
132     }
133 
134     /**
135      * Represents the parsed result.
136      */
137     public static final class ActionsModelParams {
138 
139         public static final ActionsModelParams INVALID =
140                 new ActionsModelParams(-1, "", new byte[0]);
141 
142         /**
143          * The required model version to apply {@code mSerializedPreconditions}.
144          */
145         private final int mRequiredModelVersion;
146 
147         /**
148          * The required model locales to apply {@code mSerializedPreconditions}.
149          */
150         private final String mRequiredModelLocales;
151 
152         /**
153          * The serialized params that will be applied to the model file, if all requirements are
154          * met. Do not modify.
155          */
156         private final byte[] mSerializedPreconditions;
157 
ActionsModelParams(int requiredModelVersion, String requiredModelLocales, byte[] serializedPreconditions)158         public ActionsModelParams(int requiredModelVersion, String requiredModelLocales,
159                 byte[] serializedPreconditions) {
160             mRequiredModelVersion = requiredModelVersion;
161             mRequiredModelLocales = Preconditions.checkNotNull(requiredModelLocales);
162             mSerializedPreconditions = Preconditions.checkNotNull(serializedPreconditions);
163         }
164 
165         /**
166          * Returns the serialized preconditions. Returns {@code null} if the the model in use does
167          * not meet all the requirements listed in the {@code ActionsModelParams} or the params
168          * are invalid.
169          */
170         @Nullable
getSerializedPreconditions(ModelFileManager.ModelFile modelInUse)171         public byte[] getSerializedPreconditions(ModelFileManager.ModelFile modelInUse) {
172             if (this == INVALID) {
173                 return null;
174             }
175             if (modelInUse.getVersion() != mRequiredModelVersion) {
176                 Log.w(TAG, String.format(
177                         "Not applying mSerializedPreconditions, required version=%d, actual=%d",
178                         mRequiredModelVersion, modelInUse.getVersion()));
179                 return null;
180             }
181             if (!Objects.equals(modelInUse.getSupportedLocalesStr(), mRequiredModelLocales)) {
182                 Log.w(TAG, String.format(
183                         "Not applying mSerializedPreconditions, required locales=%s, actual=%s",
184                         mRequiredModelLocales, modelInUse.getSupportedLocalesStr()));
185                 return null;
186             }
187             return mSerializedPreconditions;
188         }
189     }
190 
191     private static final class SettingsObserver extends ContentObserver {
192 
193         private final WeakReference<Runnable> mOnChangedListener;
194 
SettingsObserver(Context appContext, Runnable listener)195         SettingsObserver(Context appContext, Runnable listener) {
196             super(null);
197             mOnChangedListener = new WeakReference<>(listener);
198             appContext.getContentResolver().registerContentObserver(
199                     Settings.Global.getUriFor(Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS),
200                     false /* notifyForDescendants */,
201                     this);
202         }
203 
onChange(boolean selfChange)204         public void onChange(boolean selfChange) {
205             if (mOnChangedListener.get() != null) {
206                 mOnChangedListener.get().run();
207             }
208         }
209     }
210 }
211