1 /*
2  * Copyright (C) 2020 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.launcher3.util;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.os.UserHandle;
22 import android.util.AtomicFile;
23 import android.util.Log;
24 import android.util.Xml;
25 
26 import androidx.annotation.Nullable;
27 import androidx.annotation.WorkerThread;
28 
29 import com.android.launcher3.AutoInstallsLayout;
30 import com.android.launcher3.LauncherSettings.Favorites;
31 import com.android.launcher3.model.data.ItemInfo;
32 import com.android.launcher3.pm.UserCache;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 import org.xmlpull.v1.XmlSerializer;
37 
38 import java.io.FileInputStream;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStreamReader;
43 import java.nio.charset.StandardCharsets;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.List;
47 import java.util.function.LongFunction;
48 
49 /**
50  * Utility class to read/write a list of {@link com.android.launcher3.model.data.ItemInfo} on disk.
51  * This class is not thread safe, the caller should ensure proper threading
52  */
53 public class PersistedItemArray<T extends ItemInfo> {
54 
55     private static final String TAG = "PersistedItemArray";
56 
57     private static final String TAG_ROOT = "items";
58     private static final String TAG_ENTRY = "entry";
59 
60     private final String mFileName;
61 
PersistedItemArray(String fileName)62     public PersistedItemArray(String fileName) {
63         mFileName = fileName + ".xml";
64     }
65 
66     /**
67      * Writes the provided list of items on the disk
68      */
69     @WorkerThread
write(Context context, List<T> items)70     public void write(Context context, List<T> items) {
71         AtomicFile file = getFile(context);
72         FileOutputStream fos;
73         try {
74             fos = file.startWrite();
75         } catch (IOException e) {
76             Log.e(TAG, "Unable to persist items in " + mFileName, e);
77             return;
78         }
79 
80         UserCache userCache = UserCache.INSTANCE.get(context);
81 
82         try {
83             XmlSerializer out = Xml.newSerializer();
84             out.setOutput(fos, StandardCharsets.UTF_8.name());
85             out.startDocument(null, true);
86             out.startTag(null, TAG_ROOT);
87             for (T item : items) {
88                 Intent intent = item.getIntent();
89                 if (intent == null) {
90                     continue;
91                 }
92 
93                 out.startTag(null, TAG_ENTRY);
94                 out.attribute(null, Favorites.ITEM_TYPE, Integer.toString(item.itemType));
95                 out.attribute(null, Favorites.PROFILE_ID,
96                         Long.toString(userCache.getSerialNumberForUser(item.user)));
97                 out.attribute(null, Favorites.INTENT, intent.toUri(0));
98                 out.endTag(null, TAG_ENTRY);
99             }
100             out.endTag(null, TAG_ROOT);
101             out.endDocument();
102         } catch (IOException e) {
103             file.failWrite(fos);
104             Log.e(TAG, "Unable to persist items in " + mFileName, e);
105             return;
106         }
107 
108         file.finishWrite(fos);
109     }
110 
111     /**
112      * Reads the items from the disk
113      */
114     @WorkerThread
read(Context context, ItemFactory<T> factory)115     public List<T> read(Context context, ItemFactory<T> factory) {
116         return read(context, factory, UserCache.INSTANCE.get(context)::getUserForSerialNumber);
117     }
118 
119     /**
120      * Reads the items from the disk
121      * @param userFn method to provide user handle for a given user serial
122      */
123     @WorkerThread
read(Context context, ItemFactory<T> factory, LongFunction<UserHandle> userFn)124     public List<T> read(Context context, ItemFactory<T> factory, LongFunction<UserHandle> userFn) {
125         List<T> result = new ArrayList<>();
126         try (FileInputStream fis = getFile(context).openRead()) {
127             XmlPullParser parser = Xml.newPullParser();
128             parser.setInput(new InputStreamReader(fis, StandardCharsets.UTF_8));
129 
130             AutoInstallsLayout.beginDocument(parser, TAG_ROOT);
131             final int depth = parser.getDepth();
132 
133             int type;
134             while (((type = parser.next()) != XmlPullParser.END_TAG
135                     || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
136                 if (type != XmlPullParser.START_TAG || !TAG_ENTRY.equals(parser.getName())) {
137                     continue;
138                 }
139                 try {
140                     int itemType = Integer.parseInt(
141                             parser.getAttributeValue(null, Favorites.ITEM_TYPE));
142                     UserHandle user = userFn.apply(Long.parseLong(
143                             parser.getAttributeValue(null, Favorites.PROFILE_ID)));
144                     Intent intent = Intent.parseUri(
145                             parser.getAttributeValue(null, Favorites.INTENT), 0);
146 
147                     if (user != null && intent != null) {
148                         T item = factory.createInfo(itemType, user, intent);
149                         if (item != null) {
150                             result.add(item);
151                         }
152                     }
153                 } catch (Exception e) {
154                     // Ignore this entry
155                 }
156             }
157         } catch (FileNotFoundException e) {
158             // Ignore
159         } catch (IOException | XmlPullParserException e) {
160             Log.e(TAG, "Unable to read items in " + mFileName, e);
161             return Collections.emptyList();
162         }
163         return result;
164     }
165 
166     /**
167      * Returns the underlying file used for persisting data
168      */
getFile(Context context)169     public AtomicFile getFile(Context context) {
170         return new AtomicFile(context.getFileStreamPath(mFileName));
171     }
172 
173     /**
174      * Interface to create an ItemInfo during parsing
175      */
176     public interface ItemFactory<T extends ItemInfo> {
177 
178         /**
179          * Returns an item info or null in which case the entry is ignored
180          */
181         @Nullable
createInfo(int itemType, UserHandle user, Intent intent)182         T createInfo(int itemType, UserHandle user, Intent intent);
183     }
184 }
185