22
33import android .content .Context ;
44import android .os .Build ;
5+ import android .os .Handler ;
6+ import android .os .Looper ;
57import android .util .Log ;
68
79import org .json .JSONException ;
1618import java .nio .charset .StandardCharsets ;
1719import java .util .Date ;
1820import java .util .Locale ;
21+ import java .util .concurrent .ExecutorService ;
22+ import java .util .concurrent .Executors ;
23+ import java .util .concurrent .Future ;
24+ import java .util .concurrent .TimeUnit ;
1925
2026import io .github .vvb2060 .keyattestation .AppApplication ;
2127import io .github .vvb2060 .keyattestation .R ;
2228
2329public record RevocationList (String status , String reason , DataSource source ) {
24- public enum DataSource {
30+ public enum DataSource {
2531 NETWORK_INITIAL ,
2632 NETWORK_UPDATE ,
2733 NETWORK_UP_TO_DATE ,
@@ -38,6 +44,14 @@ public enum DataSource {
3844 private static Date publishTime = null ;
3945 private static DataSource currentSource = DataSource .BUNDLED ;
4046
47+ private static final ExecutorService asyncExecutor = Executors .newSingleThreadExecutor ();
48+ private static final ExecutorService networkExecutor = Executors .newSingleThreadExecutor ();
49+ private static final Handler mainHandler = new Handler (Looper .getMainLooper ());
50+
51+ public interface OnUpdateListener {
52+ void onUpdateSuccess (DataSource newSource );
53+ }
54+
4155 private record StatusResult (JSONObject json , DataSource source ) {}
4256 private record NetworkResult (JSONObject json , int responseCode ) {}
4357
@@ -69,6 +83,7 @@ private static void saveToCache(JSONObject fullJson) {
6983 var prefs = AppApplication .app .getSharedPreferences (PREFS_NAME , Context .MODE_PRIVATE );
7084 prefs .edit ().putLong (KEY_PUBLISH_TIME , publishTime .getTime ()).apply ();
7185 }
86+ Log .i (TAG , "Local cache file written successfully." );
7287 } catch (IOException e ) {
7388 Log .w (TAG , "Failed to cache revocation list" , e );
7489 }
@@ -80,25 +95,21 @@ private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime)
8095 URL url = new URL (statusUrl );
8196 connection = (HttpURLConnection ) url .openConnection ();
8297 connection .setRequestMethod ("GET" );
83- connection .setConnectTimeout (10000 );
84- connection .setReadTimeout (10000 );
98+ connection .setConnectTimeout (3000 );
99+ connection .setReadTimeout (3000 );
85100 connection .setRequestProperty ("User-Agent" , "KeyAttestation" );
86101
87102 double rand = Math .round (Math .random () * 1000.0 ) / 1000.0 ;
88-
89- // Force the CDN to bypass edge caches
90103 connection .setRequestProperty ("Cache-Control" , "no-cache, no-store, no-transform, max-age=0" );
91104 connection .setRequestProperty ("Accept" , "application/json, */*;q=" + rand );
92105 connection .setRequestProperty ("Accept-Encoding" , "identity, *;q=" + rand );
93106 connection .setRequestProperty ("Accept-Ranges" , "bytes" );
94107
95- // Standard conditional GET for bandwidth saving (if the node IS synced)
96108 if (cachedTime != 0 ) {
97109 connection .setIfModifiedSince (cachedTime );
98110 }
99111
100112 int responseCode = connection .getResponseCode ();
101-
102113 if (responseCode == HttpURLConnection .HTTP_NOT_MODIFIED ) {
103114 return new NetworkResult (null , responseCode );
104115 }
@@ -108,92 +119,135 @@ private static NetworkResult fetchFromNetwork(String statusUrl, long cachedTime)
108119 if (lastModified != 0 ) {
109120 publishTime = new Date (lastModified );
110121 }
111-
112122 try (var input = connection .getInputStream ()) {
113123 return new NetworkResult (parseStatus (input ), responseCode );
114124 }
115125 }
116126 return null ;
117127 } catch (Exception e ) {
118- Log .w (TAG , "Network fetch failed" , e );
119128 return null ;
120129 } finally {
121130 if (connection != null ) connection .disconnect ();
122131 }
123132 }
124133
125- private static StatusResult getStatus () {
126- var statusUrl = "https://android.googleapis.com/attestation/status" ;
127- var res = AppApplication .app .getResources ();
128- var resName = "android:string/vendor_required_attestation_revocation_list_url" ;
129- var id = res .getIdentifier (resName , null , null );
130- if (id != 0 ) {
131- var url = res .getString (id );
132- if (!statusUrl .equals (url ) && url .toLowerCase (Locale .ROOT ).startsWith ("https" )) {
133- statusUrl = url ;
134- }
134+ private static NetworkResult fetchNetworkWithTimeout (String url , long cachedTime ) {
135+ Future <NetworkResult > future = networkExecutor .submit (() -> fetchFromNetwork (url , cachedTime ));
136+ try {
137+ return future .get (3 , TimeUnit .SECONDS );
138+ } catch (Exception e ) {
139+ Log .w (TAG , "Network fetch dropped gracefully (Hard 3-second DNS/Connection Timeout)" );
140+ future .cancel (true );
141+ return null ;
135142 }
143+ }
136144
145+ private static StatusResult loadLocalData () {
137146 var prefs = AppApplication .app .getSharedPreferences (PREFS_NAME , Context .MODE_PRIVATE );
138147 long cachedTime = prefs .getLong (KEY_PUBLISH_TIME , 0 );
139148
140- // 1. Network Check
141- NetworkResult networkResult = fetchFromNetwork (statusUrl , cachedTime );
142-
143- if (networkResult != null && networkResult .responseCode () == HttpURLConnection .HTTP_NOT_MODIFIED ) {
144- try (var fis = AppApplication .app .openFileInput (CACHE_FILE )) {
145- var cacheJson = parseStatus (fis );
146- publishTime = new Date (cachedTime );
147- return new StatusResult (cacheJson .getJSONObject ("entries" ), DataSource .NETWORK_UP_TO_DATE );
148- } catch (Exception e ) {
149- Log .w (TAG , "Legacy cache format detected. Clearing and forcing fresh fetch." , e );
150-
151- // 1. Wipe the old, incompatible cache file
152- AppApplication .app .deleteFile (CACHE_FILE );
153-
154- // 2. Wipe the saved timestamp so we don't send If-Modified-Since again
155- prefs .edit ().remove (KEY_PUBLISH_TIME ).apply ();
156-
157- // 3. Immediately force a fresh 200 OK download
158- NetworkResult retryResult = fetchFromNetwork (statusUrl , 0 );
159-
160- if (retryResult != null && retryResult .json () != null ) {
161- saveToCache (retryResult .json ());
162-
163- try {
164- return new StatusResult (retryResult .json ().getJSONObject ("entries" ), DataSource .NETWORK_UPDATE );
165- } catch (JSONException je ) {
166- Log .e (TAG , "Failed to parse entries from fresh fallback fetch" , je );
167- }
168- }
169- }
170- } else if (networkResult != null && networkResult .json () != null ) {
171- saveToCache (networkResult .json ());
172- try {
173- DataSource successState = (cachedTime == 0 ) ? DataSource .NETWORK_INITIAL : DataSource .NETWORK_UPDATE ;
174- return new StatusResult (networkResult .json ().getJSONObject ("entries" ), successState );
175- } catch (JSONException ignored ) {}
176- }
177-
178- // 2. Cache
179149 try (var fis = AppApplication .app .openFileInput (CACHE_FILE )) {
180150 var cacheJson = parseStatus (fis );
181151 if (cachedTime != 0 ) publishTime = new Date (cachedTime );
152+ Log .i (TAG , "Successfully matched database schemas inside local CACHE storage." );
182153 return new StatusResult (cacheJson .getJSONObject ("entries" ), DataSource .CACHE );
183154 } catch (Exception e ) {
184- Log .i (TAG , "Cache unavailable " );
155+ Log .i (TAG , "Local file cache missing, loading baseline fallback asset. " );
185156 }
186157
187- // 3. Bundled
158+ var res = AppApplication . app . getResources ();
188159 try (var input = res .openRawResource (R .raw .status )) {
189160 var bundledJson = parseStatus (input );
190161 publishTime = null ;
191162 return new StatusResult (bundledJson .getJSONObject ("entries" ), DataSource .BUNDLED );
192163 } catch (Exception e ) {
193- throw new RuntimeException ("Critical: Failed to load revocation data " , e );
164+ throw new RuntimeException ("Critical: Baseline resource asset file missing from app payload " , e );
194165 }
195166 }
196167
168+ public static void refreshAsync (OnUpdateListener listener ) {
169+ asyncExecutor .execute (() -> {
170+ var statusUrl = "https://android.googleapis.com/attestation/status" ;
171+ var res = AppApplication .app .getResources ();
172+ var resName = "android:string/vendor_required_attestation_revocation_list_url" ;
173+ var id = res .getIdentifier (resName , null , null );
174+ if (id != 0 ) {
175+ var url = res .getString (id );
176+ if (!statusUrl .equals (url ) && url .toLowerCase (Locale .ROOT ).startsWith ("https" )) {
177+ statusUrl = url ;
178+ }
179+ }
180+
181+ var prefs = AppApplication .app .getSharedPreferences (PREFS_NAME , Context .MODE_PRIVATE );
182+ long cachedTime = prefs .getLong (KEY_PUBLISH_TIME , 0 );
183+
184+ Log .i (TAG , "Starting background network sync operation..." );
185+ NetworkResult networkResult = fetchNetworkWithTimeout (statusUrl , cachedTime );
186+ DataSource finalSource = null ;
187+ JSONObject finalEntries = null ;
188+
189+ if (networkResult != null && networkResult .responseCode () == HttpURLConnection .HTTP_NOT_MODIFIED ) {
190+ Log .i (TAG , "Network connection reporting 304 Not Modified. Local data streams up to date." );
191+ try (var fis = AppApplication .app .openFileInput (CACHE_FILE )) {
192+ var cacheJson = parseStatus (fis );
193+ publishTime = new Date (cachedTime );
194+ finalEntries = cacheJson .getJSONObject ("entries" );
195+ finalSource = DataSource .NETWORK_UP_TO_DATE ;
196+ } catch (Exception e ) {
197+ Log .w (TAG , "Legacy cache format error. Resetting storage contexts." , e );
198+ AppApplication .app .deleteFile (CACHE_FILE );
199+ prefs .edit ().remove (KEY_PUBLISH_TIME ).apply ();
200+
201+ NetworkResult retryResult = fetchNetworkWithTimeout (statusUrl , 0 );
202+ if (retryResult != null && retryResult .json () != null ) {
203+ saveToCache (retryResult .json ());
204+ try {
205+ finalEntries = retryResult .json ().getJSONObject ("entries" );
206+ finalSource = DataSource .NETWORK_UPDATE ;
207+ } catch (JSONException ignored ) {}
208+ }
209+ }
210+ } else if (networkResult != null && networkResult .json () != null ) {
211+ Log .i (TAG , "Network fetch SUCCESS! Downloaded updated production data components." );
212+ saveToCache (networkResult .json ());
213+ try {
214+ finalEntries = networkResult .json ().getJSONObject ("entries" );
215+ finalSource = (cachedTime == 0 ) ? DataSource .NETWORK_INITIAL : DataSource .NETWORK_UPDATE ;
216+ } catch (JSONException ignored ) {}
217+ } else {
218+ // CHANGE THIS: Force parsing the local cache when connection errors drop out
219+ Log .i (TAG , "Sync complete. Network timed out or dropped; falling back to offline cache." );
220+ try {
221+ StatusResult localResult = loadLocalData ();
222+ finalEntries = localResult .json ();
223+ finalSource = localResult .source (); // Becomes DataSource.CACHE
224+ } catch (Exception e ) {
225+ Log .w (TAG , "Failed to load local data fallback during offline transition" , e );
226+ }
227+ }
228+
229+ if (finalEntries != null && finalSource != null ) {
230+ final DataSource sourceToApply = finalSource ;
231+ final JSONObject entriesToApply = finalEntries ;
232+
233+ synchronized (RevocationList .class ) {
234+ data = entriesToApply ;
235+ currentSource = sourceToApply ;
236+ }
237+
238+ if (listener != null ) {
239+ mainHandler .post (() -> listener .onUpdateSuccess (sourceToApply ));
240+ }
241+ } else {
242+ Log .i (TAG , "Sync complete. Network timed out or dropped; local data streams unchanged." );
243+ }
244+ });
245+ }
246+
247+ public static void refresh () {
248+ refreshAsync (null );
249+ }
250+
197251 public static Date getPublishTime () {
198252 return publishTime ;
199253 }
@@ -202,33 +256,13 @@ public static DataSource getCurrentSource() {
202256 return currentSource ;
203257 }
204258
205- public static void refresh () {
206- synchronized (RevocationList .class ) {
207- StatusResult result = getStatus ();
208- data = result .json ();
209-
210- // If we successfully fetched a brand new file this session,
211- // don't let a subsequent UI refresh overwrite our status with a 304!
212- if ((currentSource == DataSource .NETWORK_UPDATE || currentSource == DataSource .NETWORK_INITIAL ) && result .source () == DataSource .NETWORK_UP_TO_DATE ) {
213- Log .i (TAG , "Preserving NETWORK_UPDATE status across multiple refreshes" );
214- } else {
215- currentSource = result .source ();
216- }
217- }
218- }
219-
220259 public static RevocationList get (BigInteger serialNumber ) {
221260 if (data == null ) {
222261 synchronized (RevocationList .class ) {
223262 if (data == null ) {
224- StatusResult result = getStatus ();
263+ StatusResult result = loadLocalData ();
225264 data = result .json ();
226-
227- if ((currentSource == DataSource .NETWORK_UPDATE || currentSource == DataSource .NETWORK_INITIAL ) && result .source () == DataSource .NETWORK_UP_TO_DATE ) {
228- Log .i (TAG , "Preserving NETWORK_UPDATE status in get()" );
229- } else {
230- currentSource = result .source ();
231- }
265+ currentSource = result .source ();
232266 }
233267 }
234268 }
@@ -245,4 +279,4 @@ public static RevocationList get(BigInteger serialNumber) {
245279 public String toString () {
246280 return "status: " + status + ", source: " + source ;
247281 }
248- }
282+ }
0 commit comments