package com.topjohnwu.magisk.asyncs; import android.database.Cursor; import android.text.TextUtils; import com.topjohnwu.magisk.MagiskManager; import com.topjohnwu.magisk.ReposFragment; import com.topjohnwu.magisk.container.Repo; import com.topjohnwu.magisk.utils.Const; import com.topjohnwu.magisk.utils.Logger; import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.magisk.utils.WebService; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.HttpURLConnection; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class UpdateRepos extends ParallelTask { private static final int CHECK_ETAG = 0; private static final int LOAD_NEXT = 1; private static final int LOAD_PREV = 2; private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = Math.max(2, CPU_COUNT - 1); private MagiskManager mm; private List etags, newEtags = new LinkedList<>(); private Set cached; private boolean forceUpdate; private ExecutorService threadPool; public UpdateRepos(boolean force) { mm = MagiskManager.get(); mm.repoLoadDone.reset(); forceUpdate = force; threadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE); } private void waitTasks() { threadPool.shutdown(); try { threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); } catch (InterruptedException ignored) {} } private boolean loadJSON(String jsonString) throws JSONException, ParseException { JSONArray jsonArray = new JSONArray(jsonString); // Empty page, halt if (jsonArray.length() == 0) return false; for (int i = 0; i < jsonArray.length(); i++) { JSONObject rawRepo = jsonArray.getJSONObject(i); String id = rawRepo.getString("description"); String name = rawRepo.getString("name"); Date date = dateFormat.parse(rawRepo.getString("pushed_at")); Set set = Collections.synchronizedSet(cached); threadPool.execute(() -> { Repo repo = mm.repoDB.getRepo(id); try { if (repo == null) repo = new Repo(name); else set.remove(id); repo.update(date); mm.repoDB.addRepo(repo); publishProgress(); } catch (Repo.IllegalRepoException e) { Logger.debug(e.getMessage()); mm.repoDB.removeRepo(id); } }); } return true; } private boolean loadPage(int page, int mode) { Map header = new HashMap<>(); if (mode == CHECK_ETAG && page < etags.size()) header.put(Const.Key.IF_NONE_MATCH, etags.get(page)); String url = Utils.fmt(Const.Url.REPO_URL, page + 1); try { HttpURLConnection conn = WebService.request(url, header); if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { // Current page is not updated, check the next page return loadPage(page + 1, CHECK_ETAG); } if (!loadJSON(WebService.getString(conn))) return mode != CHECK_ETAG; } catch (Exception e) { e.printStackTrace(); return false; } /* If one page is updated, we force update all pages */ // Update ETAG String etag = header.get(Const.Key.ETAG_KEY); etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1); if (mode == LOAD_PREV) { // We are loading a previous page, push the new tag to the front newEtags.add(0, etag); } else { newEtags.add(etag); } String links = header.get(Const.Key.LINK_KEY); if (links != null) { for (String s : links.split(", ")) { if (mode != LOAD_PREV && s.contains("next")) { // Force load all next pages loadPage(page + 1, LOAD_NEXT); } if (mode != LOAD_NEXT && s.contains("prev")) { // Back propagation loadPage(page - 1, LOAD_PREV); } } } return true; } @Override protected void onProgressUpdate(Void... values) { if (ReposFragment.adapter != null) ReposFragment.adapter.notifyDBChanged(); } @Override protected void onPreExecute() { mm.repoLoadDone.setPending(); } @Override protected Void doInBackground(Void... voids) { etags = Arrays.asList(mm.prefs.getString(Const.Key.ETAG_KEY, "").split(",")); cached = mm.repoDB.getRepoIDSet(); if (loadPage(0, CHECK_ETAG)) { waitTasks(); // The leftover cached means they are removed from online repo mm.repoDB.removeRepo(cached); // Update ETag mm.prefs.edit().putString(Const.Key.ETAG_KEY, TextUtils.join(",", newEtags)).apply(); } else if (forceUpdate) { Cursor c = mm.repoDB.getRawCursor(); while (c.moveToNext()) { Repo repo = new Repo(c); threadPool.execute(() -> { try { repo.update(); mm.repoDB.addRepo(repo); } catch (Repo.IllegalRepoException e) { Logger.debug(e.getMessage()); mm.repoDB.removeRepo(repo); } }); } waitTasks(); } return null; } @Override protected void onPostExecute(Void v) { mm.repoLoadDone.publish(); super.onPostExecute(v); } }