1 /*
2  * Copyright 2018 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 androidx.slice.compat;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.net.Uri;
22 import android.os.SystemClock;
23 import android.text.TextUtils;
24 
25 import androidx.annotation.RestrictTo;
26 import androidx.annotation.VisibleForTesting;
27 import androidx.collection.ArraySet;
28 import androidx.core.util.ObjectsCompat;
29 import androidx.slice.SliceSpec;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Set;
34 
35 /**
36  * Tracks the current packages requesting pinning of any given slice. It will clear the
37  * list after a reboot since the packages are no longer requesting pinning.
38  * @hide
39  */
40 @RestrictTo(RestrictTo.Scope.LIBRARY)
41 public class CompatPinnedList {
42 
43     private static final String LAST_BOOT = "last_boot";
44     private static final String PIN_PREFIX = "pinned_";
45     private static final String SPEC_NAME_PREFIX = "spec_names_";
46     private static final String SPEC_REV_PREFIX = "spec_revs_";
47 
48     // Max skew between bootup times that we think its probably rebooted.
49     // There could be some difference in our calculated boot up time if the thread
50     // sleeps between currentTimeMillis and elapsedRealtime.
51     // Its probably safe to assume the device can't boot twice within 2 secs.
52     private static final long BOOT_THRESHOLD = 2000;
53 
54     private final Context mContext;
55     private final String mPrefsName;
56 
CompatPinnedList(Context context, String prefsName)57     public CompatPinnedList(Context context, String prefsName) {
58         mContext = context;
59         mPrefsName = prefsName;
60     }
61 
getPrefs()62     private SharedPreferences getPrefs() {
63         SharedPreferences prefs = mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
64         long lastBootTime = prefs.getLong(LAST_BOOT, 0);
65         long currentBootTime = getBootTime();
66         if (Math.abs(lastBootTime - currentBootTime) > BOOT_THRESHOLD) {
67             prefs.edit()
68                     .clear()
69                     .putLong(LAST_BOOT, currentBootTime)
70                     .commit();
71         }
72         return prefs;
73     }
74 
75     /**
76      * Get pinned specs
77      */
getPinnedSlices()78     public List<Uri> getPinnedSlices() {
79         List<Uri> pinned = new ArrayList<>();
80         for (String key : getPrefs().getAll().keySet()) {
81             if (key.startsWith(PIN_PREFIX)) {
82                 Uri uri = Uri.parse(key.substring(PIN_PREFIX.length()));
83                 if (!getPins(uri).isEmpty()) {
84                     pinned.add(uri);
85                 }
86             }
87         }
88         return pinned;
89     }
90 
getPins(Uri uri)91     private Set<String> getPins(Uri uri) {
92         return getPrefs().getStringSet(PIN_PREFIX + uri.toString(), new ArraySet<String>());
93     }
94 
95     /**
96      * Get the list of specs for a pinned Uri.
97      */
getSpecs(Uri uri)98     public synchronized ArraySet<SliceSpec> getSpecs(Uri uri) {
99         ArraySet<SliceSpec> specs = new ArraySet<>();
100         SharedPreferences prefs = getPrefs();
101         String specNamesStr = prefs.getString(SPEC_NAME_PREFIX + uri.toString(), null);
102         String specRevsStr = prefs.getString(SPEC_REV_PREFIX + uri.toString(), null);
103         if (TextUtils.isEmpty(specNamesStr) || TextUtils.isEmpty(specRevsStr)) {
104             return new ArraySet<>();
105         }
106         String[] specNames = specNamesStr.split(",", -1);
107         String[] specRevs = specRevsStr.split(",", -1);
108         if (specNames.length != specRevs.length) {
109             return new ArraySet<>();
110         }
111         for (int i = 0; i < specNames.length; i++) {
112             specs.add(new SliceSpec(specNames[i], Integer.parseInt(specRevs[i])));
113         }
114         return specs;
115     }
116 
setPins(Uri uri, Set<String> pins)117     private void setPins(Uri uri, Set<String> pins) {
118         getPrefs().edit()
119                 .putStringSet(PIN_PREFIX + uri.toString(), pins)
120                 .commit();
121     }
122 
setSpecs(Uri uri, ArraySet<SliceSpec> specs)123     private void setSpecs(Uri uri, ArraySet<SliceSpec> specs) {
124         String[] specNames = new String[specs.size()];
125         String[] specRevs = new String[specs.size()];
126         for (int i = 0; i < specs.size(); i++) {
127             specNames[i] = specs.valueAt(i).getType();
128             specRevs[i] = String.valueOf(specs.valueAt(i).getRevision());
129         }
130         getPrefs().edit()
131                 .putString(SPEC_NAME_PREFIX + uri.toString(), TextUtils.join(",", specNames))
132                 .putString(SPEC_REV_PREFIX + uri.toString(), TextUtils.join(",", specRevs))
133                 .commit();
134     }
135 
136     @VisibleForTesting
getBootTime()137     protected long getBootTime() {
138         return System.currentTimeMillis() - SystemClock.elapsedRealtime();
139     }
140 
141     /**
142      * Adds a pin for a specific uri/pkg pair and returns true if the
143      * uri was not previously pinned.
144      */
addPin(Uri uri, String pkg, Set<SliceSpec> specs)145     public synchronized boolean addPin(Uri uri, String pkg, Set<SliceSpec> specs) {
146         Set<String> pins = getPins(uri);
147         boolean wasNotPinned = pins.isEmpty();
148         pins.add(pkg);
149         setPins(uri, pins);
150         if (wasNotPinned) {
151             setSpecs(uri, new ArraySet<>(specs));
152         } else {
153             setSpecs(uri, mergeSpecs(getSpecs(uri), specs));
154         }
155         return wasNotPinned;
156     }
157 
158     /**
159      * Removes a pin for a specific uri/pkg pair and returns true if the
160      * uri is no longer pinned (but was).
161      */
removePin(Uri uri, String pkg)162     public synchronized boolean removePin(Uri uri, String pkg) {
163         Set<String> pins = getPins(uri);
164         if (pins.isEmpty() || !pins.contains(pkg)) {
165             return false;
166         }
167         pins.remove(pkg);
168         setPins(uri, pins);
169         return pins.size() == 0;
170     }
171 
mergeSpecs(ArraySet<SliceSpec> specs, Set<SliceSpec> supportedSpecs)172     private static ArraySet<SliceSpec> mergeSpecs(ArraySet<SliceSpec> specs,
173             Set<SliceSpec> supportedSpecs) {
174         for (int i = 0; i < specs.size(); i++) {
175             SliceSpec s = specs.valueAt(i);
176             SliceSpec other = findSpec(supportedSpecs, s.getType());
177             if (other == null) {
178                 specs.removeAt(i--);
179             } else if (other.getRevision() < s.getRevision()) {
180                 specs.removeAt(i--);
181                 specs.add(other);
182             }
183         }
184         return specs;
185     }
186 
findSpec(Set<SliceSpec> specs, String type)187     private static SliceSpec findSpec(Set<SliceSpec> specs, String type) {
188         for (SliceSpec spec : specs) {
189             if (ObjectsCompat.equals(spec.getType(), type)) {
190                 return spec;
191             }
192         }
193         return null;
194     }
195 }
196