/**************************************************************************************** * Copyright (c) 2012 Kostas Spyropoulos <inigo.aldana@gmail.com> * * Copyright (c) 2014 Houssam Salem <houssam.salem.au@gmail.com> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 3 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.ichi2.libanki.sync; import android.content.SharedPreferences; import android.net.Uri; import android.text.TextUtils; import com.ichi2.anki.AnkiDroidApp; import com.ichi2.anki.exception.MediaSyncException; import com.ichi2.anki.exception.UnknownHttpResponseException; import com.ichi2.async.Connection; import com.ichi2.libanki.Collection; import com.ichi2.libanki.Consts; import com.ichi2.libanki.Utils; import com.ichi2.utils.VersionUtils; import org.apache.http.HttpResponse; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.zip.ZipFile; import timber.log.Timber; public class RemoteMediaServer extends HttpSyncer { private Collection mCol; public RemoteMediaServer(Collection col, String hkey, Connection con) { super(hkey, con); mCol = col; } @Override public String syncURL() { // Allow user to specify custom sync server SharedPreferences userPreferences = AnkiDroidApp.getSharedPrefs(AnkiDroidApp.getInstance()); if (userPreferences!= null && userPreferences.getBoolean("useCustomSyncServer", false)) { Uri mediaSyncBase = Uri.parse(userPreferences.getString("syncMediaUrl", Consts.SYNC_MEDIA_BASE)); return mediaSyncBase.toString() + "/"; } // Usual case return Consts.SYNC_MEDIA_BASE; } public JSONObject begin() throws UnknownHttpResponseException, MediaSyncException { try { mPostVars = new HashMap<>(); mPostVars.put("k", mHKey); mPostVars.put("v", String.format(Locale.US, "ankidroid,%s,%s", VersionUtils.getPkgVersionName(), Utils.platDesc())); HttpResponse resp = super.req("begin", super.getInputStream(Utils.jsonToString(new JSONObject()))); JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent())); JSONObject ret = _dataOnly(jresp, JSONObject.class); mSKey = ret.getString("sk"); return ret; } catch (JSONException | IOException e) { throw new RuntimeException(e); } } // args: lastUsn public JSONArray mediaChanges(int lastUsn) throws UnknownHttpResponseException, MediaSyncException { try { mPostVars = new HashMap<>(); mPostVars.put("sk", mSKey); HttpResponse resp = super.req("mediaChanges", super.getInputStream(Utils.jsonToString(new JSONObject().put("lastUsn", lastUsn)))); JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent())); return _dataOnly(jresp, JSONArray.class); } catch (JSONException | IOException e) { throw new RuntimeException(e); } } /** * args: files * <br> * This method returns a ZipFile with the OPEN_DELETE flag, ensuring that the file on disk will * be automatically deleted when the stream is closed. */ public ZipFile downloadFiles(List<String> top) throws UnknownHttpResponseException { try { HttpResponse resp; resp = super.req("downloadFiles", super.getInputStream(Utils.jsonToString(new JSONObject().put("files", new JSONArray(top))))); String zipPath = mCol.getPath().replaceFirst("collection\\.anki2$", "tmpSyncFromServer.zip"); // retrieve contents and save to file on disk: super.writeToFile(resp.getEntity().getContent(), zipPath); return new ZipFile(new File(zipPath), ZipFile.OPEN_READ | ZipFile.OPEN_DELETE); } catch (JSONException e) { throw new RuntimeException(e); } catch (IOException e) { Timber.e(e, "Failed to download requested media files"); throw new RuntimeException(e); } } public JSONArray uploadChanges(File zip) throws UnknownHttpResponseException, MediaSyncException { try { // no compression, as we compress the zip file instead HttpResponse resp = super.req("uploadChanges", new FileInputStream(zip), 0); JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent())); return _dataOnly(jresp, JSONArray.class); } catch (JSONException | IOException e) { throw new RuntimeException(e); } } // args: local public String mediaSanity(int lcnt) throws UnknownHttpResponseException, MediaSyncException { try { HttpResponse resp = super.req("mediaSanity", super.getInputStream(Utils.jsonToString(new JSONObject().put("local", lcnt)))); JSONObject jresp = new JSONObject(super.stream2String(resp.getEntity().getContent())); return _dataOnly(jresp, String.class); } catch (JSONException | IOException e) { throw new RuntimeException(e); } } /** * Returns the "data" element from the JSON response from the server, or throws an exception if there is a value in * the "err" element. * <p> * The python counterpart to this method is flexible with type coercion; the type of object returned is decided by * the content of the "data" element, and there are several such types in the various server responses. Java * requires us to specifically choose a type to convert to, so we need an additional parameter (returnType) to * specify the type we expect. * * @param resp The JSON response from the server * @param returnType The type to coerce the 'data' element to. * @return The "data" element from the HTTP response from the server. The type of object returned is determined by * returnType. */ @SuppressWarnings("unchecked") private <T> T _dataOnly(JSONObject resp, Class<T> returnType) throws MediaSyncException { try { if (!TextUtils.isEmpty(resp.optString("err"))) { String err = resp.getString("err"); mCol.log("error returned: " + err); throw new MediaSyncException("SyncError:" + err); } if (returnType == String.class) { return (T) resp.getString("data"); } else if (returnType == JSONObject.class) { return (T) resp.getJSONObject("data"); } else if (returnType == JSONArray.class) { return (T) resp.getJSONArray("data"); } throw new RuntimeException("Did not specify a valid type for the 'data' element in resopnse"); } catch (JSONException e) { throw new RuntimeException(e); } } }