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