1 /*
2  * Copyright (C) 2021 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 android.content.pm;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Person;
22 import android.app.appsearch.AppSearchSchema;
23 import android.app.appsearch.GenericDocument;
24 import android.graphics.drawable.Icon;
25 import android.net.UriCodec;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.IOException;
32 import java.nio.charset.StandardCharsets;
33 import java.util.Objects;
34 import java.util.UUID;
35 
36 /**
37  * A {@link GenericDocument} representation of {@link Person} object.
38  *
39  * @hide
40  */
41 public class AppSearchShortcutPerson extends GenericDocument {
42 
43     /**
44      * The name of the schema type for {@link Person} documents.
45      * @hide
46      */
47     public static final String SCHEMA_TYPE = "ShortcutPerson";
48 
49     private static final String KEY_NAME = "name";
50     private static final String KEY_KEY = "key";
51     private static final String KEY_IS_BOT = "isBot";
52     private static final String KEY_IS_IMPORTANT = "isImportant";
53     private static final String KEY_ICON = "icon";
54 
AppSearchShortcutPerson(@onNull GenericDocument document)55     public AppSearchShortcutPerson(@NonNull GenericDocument document) {
56         super(document);
57     }
58 
59     public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
60             .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_NAME)
61                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
62                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
63                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
64                     .build()
65 
66             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_KEY)
67                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
68                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
69                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
70                     .build()
71 
72             ).addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(KEY_IS_BOT)
73                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
74                     .build()
75 
76             ).addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(KEY_IS_IMPORTANT)
77                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
78                     .build()
79 
80             ).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(KEY_ICON)
81                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
82                     .build()
83 
84             ).build();
85 
86     /** @hide */
87     @NonNull
instance(@onNull final Person person)88     public static AppSearchShortcutPerson instance(@NonNull final Person person) {
89         Objects.requireNonNull(person);
90         final String id;
91         if (person.getUri() != null) {
92             id = person.getUri();
93         } else {
94             // NOTE: an identifier is required even when uri is null.
95             id = UUID.randomUUID().toString();
96         }
97         return new Builder(id).setName(person.getName())
98                 .setKey(person.getKey()).setIsBot(person.isBot())
99                 .setIsImportant(person.isImportant())
100                 .setIcon(transformToByteArray(person.getIcon())).build();
101     }
102 
103     /**
104      * Convert this {@link GenericDocument} into {@link Person}.
105      */
106     @NonNull
toPerson()107     public Person toPerson() {
108         String uri;
109         try {
110             uri = UriCodec.decode(
111                     getId(), false /* convertPlus */, StandardCharsets.UTF_8,
112                     true /* throwOnFailure */);
113         } catch (IllegalArgumentException e) {
114             uri = null;
115         }
116         return new Person.Builder().setName(getPropertyString(KEY_NAME))
117                 .setUri(uri).setKey(getPropertyString(KEY_KEY))
118                 .setBot(getPropertyBoolean(KEY_IS_BOT))
119                 .setImportant(getPropertyBoolean(KEY_IS_IMPORTANT))
120                 .setIcon(transformToIcon(getPropertyBytes(KEY_ICON)))
121                 .build();
122     }
123 
124     /** @hide */
125     @VisibleForTesting
126     public static class Builder extends GenericDocument.Builder<Builder> {
127 
Builder(@onNull final String id)128         public Builder(@NonNull final String id) {
129             super(/*namespace=*/ "", id, SCHEMA_TYPE);
130         }
131 
132         /** @hide */
133         @NonNull
setName(@ullable final CharSequence name)134         public Builder setName(@Nullable final CharSequence name) {
135             if (name != null) {
136                 setPropertyString(KEY_NAME, name.toString());
137             }
138             return this;
139         }
140 
141         /** @hide */
142         @NonNull
setKey(@ullable final String key)143         public Builder setKey(@Nullable final String key) {
144             if (key != null) {
145                 setPropertyString(KEY_KEY, key);
146             }
147             return this;
148         }
149 
150         /** @hide */
151         @NonNull
setIsBot(final boolean isBot)152         public Builder setIsBot(final boolean isBot) {
153             setPropertyBoolean(KEY_IS_BOT, isBot);
154             return this;
155         }
156 
157         /** @hide */
158         @NonNull
setIsImportant(final boolean isImportant)159         public Builder setIsImportant(final boolean isImportant) {
160             setPropertyBoolean(KEY_IS_IMPORTANT, isImportant);
161             return this;
162         }
163 
164         /** @hide */
165         @NonNull
setIcon(@ullable final byte[] icon)166         public Builder setIcon(@Nullable final byte[] icon) {
167             if (icon != null) {
168                 setPropertyBytes(KEY_ICON, icon);
169             }
170             return this;
171         }
172 
173         /** @hide */
174         @NonNull
175         @Override
build()176         public AppSearchShortcutPerson build() {
177             return new AppSearchShortcutPerson(super.build());
178         }
179     }
180 
181     /**
182      * Convert {@link Icon} into byte[].
183      */
184     @Nullable
transformToByteArray(@ullable final Icon icon)185     private static byte[] transformToByteArray(@Nullable final Icon icon) {
186         if (icon == null) {
187             return null;
188         }
189         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
190             icon.writeToStream(baos);
191             return baos.toByteArray();
192         } catch (IOException e) {
193             return null;
194         }
195     }
196 
197     /**
198      * Convert byte[] into {@link Icon}.
199      */
200     @Nullable
transformToIcon(@ullable final byte[] icon)201     private Icon transformToIcon(@Nullable final byte[] icon) {
202         if (icon == null) {
203             return null;
204         }
205         try (ByteArrayInputStream bais = new ByteArrayInputStream(icon)) {
206             return Icon.createFromStream(bais);
207         } catch (IOException e) {
208             return null;
209         }
210     }
211 }
212