// // Copyright (c) 2014 VK.com // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // package com.vk.sdk.api; import android.content.Context; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import com.android.annotations.Nullable; import android.util.Log; import com.vk.sdk.VKAccessToken; import com.vk.sdk.VKObject; import com.vk.sdk.VKSdk; import com.vk.sdk.VKServiceActivity; import com.vk.sdk.VKUIHelper; import com.vk.sdk.api.httpClient.VKAbstractOperation; import com.vk.sdk.api.httpClient.VKHttpClient; import com.vk.sdk.api.httpClient.VKHttpOperation; import com.vk.sdk.api.httpClient.VKJsonOperation; import com.vk.sdk.api.httpClient.VKModelOperation; import com.vk.sdk.api.model.VKApiModel; import com.vk.sdk.util.VKStringJoiner; import com.vk.sdk.util.VKUtil; import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; /** * Class for execution API-requests. */ public class VKRequest extends VKObject { public enum VKProgressType { Download, Upload } @Deprecated public enum HttpMethod { GET, POST } public final Context context; /** * Selected method name */ public final String methodName; /** * Passed parameters for method */ private final VKParameters mMethodParameters; /** * Method parametes with common parameters */ private VKParameters mPreparedParameters; /** * HTTP loading operation */ private VKAbstractOperation mLoadingOperation; /** * How much times request was loaded */ private int mAttemptsUsed; /** * Requests that should be called after current request. */ private ArrayList<VKRequest> mPostRequestsQueue; /** * Class for model parsing */ private Class<? extends VKApiModel> mModelClass; /** * Response parser */ private VKParser mModelParser; /** * Specify language for API request */ private String mPreferredLang; private boolean mUseLooperForCallListener = true; /** * Looper which starts request */ private Looper mLooper; /** * Specify listener for current request */ @Nullable public VKRequestListener requestListener; /** * Specify if this request should present an activity for processing errors, which required user interaction. */ public boolean shouldInterruptUI; /** * Specify attempts for request loading if caused HTTP-error. 0 for infinite */ public int attempts; /** * Use HTTPS requests (by default is YES). If http-request is impossible (user denied no https access), SDK will load https version */ public boolean secure; /** * Sets current system language as default for API data */ public boolean useSystemLanguage; /** * Set to false if you don't need automatic model parsing */ public boolean parseModel; /** * Response for this request */ public WeakReference<VKResponse> response; /** * @return Returns list of method parameters (without common parameters) */ public VKParameters getMethodParameters() { return mMethodParameters; } /** * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods * * @param method API-method name, e.g. audio.get */ public VKRequest(String method) { this(method, null); } /** * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods * * @param method API-method name, e.g. audio.get * @param parameters method parameters */ public VKRequest(String method, VKParameters parameters) { this(method, parameters, null); } @Deprecated public VKRequest(String method, VKParameters parameters, HttpMethod httpMethod, Class<? extends VKApiModel> modelClass) { this(method, parameters, modelClass); } /** * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods * * @param method API-method name, e.g. audio.get * @param parameters method parameters * @param modelClass class for automatic parse */ public VKRequest(String method, VKParameters parameters, Class<? extends VKApiModel> modelClass) { this.context = VKUIHelper.getApplicationContext(); this.methodName = method; if (parameters == null) { parameters = new VKParameters(); } this.mMethodParameters = new VKParameters(parameters); this.mAttemptsUsed = 0; this.secure = true; //By default there is 1 attempt for loading. this.attempts = 1; //If system language is not supported, we use english this.mPreferredLang = "en"; //By default we use system language. this.useSystemLanguage = true; this.shouldInterruptUI = true; setModelClass(modelClass); } public void setUseLooperForCallListener(boolean useLooperForCallListener) { this.mUseLooperForCallListener = useLooperForCallListener; } /** * Executes that request, and returns result to blocks * * @param listener listener for request events */ public void executeWithListener(VKRequestListener listener) { this.requestListener = listener; start(); } /** * Executes that request, and returns result to blocks * * @param listener listener for request events */ public void executeSyncWithListener(VKRequestListener listener) { VKSyncRequestUtil.executeSyncWithListener(this, listener); } public void setRequestListener(@Nullable VKRequestListener listener) { this.requestListener = listener; } /** * Register current request for execute after passed request, if passed request is successful. If it's not, errorBlock will be called. * * @param request after which request must be called that request * @param listener listener for request events */ public void executeAfterRequest(VKRequest request, VKRequestListener listener) { this.requestListener = listener; request.addPostRequest(this); } private void addPostRequest(VKRequest postRequest) { if (mPostRequestsQueue == null) { mPostRequestsQueue = new ArrayList<>(); } mPostRequestsQueue.add(postRequest); } public VKParameters getPreparedParameters() { if (mPreparedParameters == null) { mPreparedParameters = new VKParameters(mMethodParameters); //Set current access token from SDK object VKAccessToken token = VKAccessToken.currentToken(); if (token != null) { mPreparedParameters.put(VKApiConst.ACCESS_TOKEN, token.accessToken); if (token.httpsRequired) { this.secure = true; } } //Set actual version of API mPreparedParameters.put(VKApiConst.VERSION, VKSdk.getApiVersion()); //Set preferred language for request mPreparedParameters.put(VKApiConst.LANG, getLang()); if (this.secure) { //If request is secure, we need all urls as https mPreparedParameters.put(VKApiConst.HTTPS, "1"); } if (token != null && token.secret != null) { //If it not, generate signature of request String sig = generateSig(token); mPreparedParameters.put(VKApiConst.SIG, sig); } //From that moment you cannot modify parameters. //Specially for http loading } return mPreparedParameters; } /** * Prepares request for loading * * @return Prepared HttpUriRequest for that VKRequest */ public VKHttpClient.VKHTTPRequest getPreparedRequest() { VKHttpClient.VKHTTPRequest request = VKHttpClient.requestWithVkRequest(this); if (request == null) { VKError error = new VKError(VKError.VK_REQUEST_NOT_PREPARED); provideError(error); return null; } return request; } VKAbstractOperation getOperation() { if (this.parseModel) { if (this.mModelClass != null) { mLoadingOperation = new VKModelOperation(getPreparedRequest(), this.mModelClass); } else if (this.mModelParser != null) { mLoadingOperation = new VKModelOperation(getPreparedRequest(), this.mModelParser); } } if (mLoadingOperation == null) { mLoadingOperation = new VKJsonOperation(getPreparedRequest()); } if (mLoadingOperation instanceof VKHttpOperation) { ((VKHttpOperation) mLoadingOperation).setHttpOperationListener(getHttpListener()); } return mLoadingOperation; } private VKJsonOperation.VKJSONOperationCompleteListener getHttpListener() { return new VKJsonOperation.VKJSONOperationCompleteListener() { @Override public void onComplete(VKJsonOperation operation, JSONObject response) { if (response.has("error")) { try { VKError error = new VKError(response.getJSONObject("error")); if (VKSdk.DEBUG && VKSdk.DEBUG_API_ERRORS) { Log.w(VKSdk.SDK_TAG, operation.getResponseString()); } if (processCommonError(error)) { return; } provideError(error); } catch (JSONException e) { if (VKSdk.DEBUG) e.printStackTrace(); } return; } provideResponse(response, mLoadingOperation instanceof VKModelOperation ? ((VKModelOperation) mLoadingOperation).parsedModel : null); } @Override public void onError(VKJsonOperation operation, VKError error) { //Хак для проверки того, что корректно распарсился ответ при заливке картинок if (error.errorCode != VKError.VK_CANCELED && error.errorCode != VKError.VK_API_ERROR && operation != null && operation.response != null && operation.response.statusCode == 200) { provideResponse(operation.getResponseJson(), null); return; } if (VKSdk.DEBUG && VKSdk.DEBUG_API_ERRORS && operation != null && operation.getResponseString() != null) { Log.w(VKSdk.SDK_TAG, operation.getResponseString()); } if (attempts == 0 || ++mAttemptsUsed < attempts) { if (requestListener != null) requestListener.attemptFailed(VKRequest.this, mAttemptsUsed, attempts); runOnLooper(new Runnable() { @Override public void run() { start(); } }, 300); return; } provideError(error); } }; } /** * Starts loading of prepared request. You can use it instead of executeWithResultBlock */ public void start() { if ((mLoadingOperation = getOperation()) == null) { return; } if (mLooper == null) { mLooper = Looper.myLooper(); } VKHttpClient.enqueueOperation(mLoadingOperation); } /** * Repeats this request with initial parameters and blocks. * Used attempts will be set to 0. */ public void repeat() { this.mAttemptsUsed = 0; this.mPreparedParameters = null; this.mLoadingOperation = null; start(); } /** * Cancel current request. Result will be not passed. errorBlock will be called with error code */ public void cancel() { if (mLoadingOperation != null) { mLoadingOperation.cancel(); } else { provideError(new VKError(VKError.VK_CANCELED)); } } /** * Method used for errors processing * * @param error error caused by this request */ private void provideError(final VKError error) { error.request = this; final boolean useLooperForCallListener = mUseLooperForCallListener; if (!useLooperForCallListener && requestListener != null) { requestListener.onError(error); } runOnLooper(new Runnable() { @Override public void run() { if (useLooperForCallListener && requestListener != null) { requestListener.onError(error); } if (mPostRequestsQueue != null && mPostRequestsQueue.size() > 0) { for (VKRequest postRequest : mPostRequestsQueue) if (postRequest.requestListener != null) postRequest.requestListener.onError(error); } } }); } /** * Method used for response processing * * @param jsonResponse response from API * @param parsedModel model parsed from json */ private void provideResponse(final JSONObject jsonResponse, Object parsedModel) { final VKResponse response = new VKResponse(); response.request = this; response.json = jsonResponse; response.parsedModel = parsedModel; this.response = new WeakReference<>(response); if (mLoadingOperation instanceof VKHttpOperation) { response.responseString = ((VKHttpOperation) mLoadingOperation).getResponseString(); } final boolean useLooperForCallListener = mUseLooperForCallListener; runOnLooper(new Runnable() { @Override public void run() { if (mPostRequestsQueue != null && mPostRequestsQueue.size() > 0) { for (final VKRequest request : mPostRequestsQueue) { request.start(); } } if (useLooperForCallListener && requestListener != null) { requestListener.onComplete(response); } } }); if (!useLooperForCallListener && requestListener != null) { requestListener.onComplete(response); } } /** * Adds additional parameter to that request * * @param key parameter name * @param value parameter value */ public void addExtraParameter(String key, Object value) { mMethodParameters.put(key, value); } /** * Adds additional parameters to that request * * @param extraParameters parameters supposed to be added */ public void addExtraParameters(VKParameters extraParameters) { mMethodParameters.putAll(extraParameters); } private String generateSig(VKAccessToken token) { //Read description here https://vk.com/dev/api_nohttps //At first, we need key-value pairs in order of request String queryString = VKStringJoiner.joinParams(mPreparedParameters); //Then we generate "request string" /method/{METHOD_NAME}?{GET_PARAMS}{POST_PARAMS} queryString = String.format(Locale.US, "/method/%s?%s", methodName, queryString); return VKUtil.md5(queryString + token.secret); } private boolean processCommonError(final VKError error) { //TODO: lock thread, if ui required, release then if (error.errorCode == VKError.VK_API_ERROR) { final VKError apiError = error.apiError; VKSdk.notifySdkAboutApiError(apiError); if (apiError.errorCode == 16) { VKAccessToken token = VKAccessToken.currentToken(); if (token != null) { token.httpsRequired = true; token.save(); } repeat(); return true; } else if (shouldInterruptUI) { apiError.request = this; if (error.apiError.errorCode == 14) { this.mLoadingOperation = null; VKServiceActivity.interruptWithError(context, apiError, VKServiceActivity.VKServiceType.Captcha); return true; } else if (apiError.errorCode == 17) { VKServiceActivity.interruptWithError(context, apiError, VKServiceActivity.VKServiceType.Validation); return true; } } } return false; } private String getLang() { String result = mPreferredLang; Resources res = Resources.getSystem(); if (useSystemLanguage && res != null) { result = res.getConfiguration().locale.getLanguage(); if (result.equals("uk")) { result = "ua"; } if (!Arrays.asList(new String[]{"ru", "en", "ua", "es", "fi", "de", "it"}) .contains(result)) { result = mPreferredLang; } } return result; } /** * Sets preferred language for api results. * * @param lang Two letter language code. May be "ru", "en", "ua", "es", "fi", "de", "it" */ public void setPreferredLang(String lang) { useSystemLanguage = false; mPreferredLang = lang; } /** * Sets class for parse object model * * @param modelClass Class extends VKApiModel */ public void setModelClass(Class<? extends VKApiModel> modelClass) { mModelClass = modelClass; if (mModelClass != null) parseModel = true; } /** * Specify parser for response json, which creates data model * * @param parser */ public void setResponseParser(VKParser parser) { mModelParser = parser; if (mModelParser != null) { parseModel = true; } } private void runOnLooper(Runnable block) { runOnLooper(block, 0); } private void runOnLooper(Runnable block, int delay) { if (mLooper == null) { mLooper = Looper.getMainLooper(); } if (delay > 0) { new Handler(mLooper).postDelayed(block, delay); } else { new Handler(mLooper).post(block); } } private void runOnMainLooper(Runnable block) { new Handler(Looper.getMainLooper()).post(block); } /** * Extend listeners for requests from that class * Created by Roman Truba on 02.12.13. * Copyright (c) 2013 VK. All rights reserved. */ public static abstract class VKRequestListener { /** * Called if there were no HTTP or API errors, returns execution result. * * @param response response from VKRequest */ public void onComplete(VKResponse response) { } /** * Called when request has failed attempt, and ready to do next attempt * * @param request Failed request * @param attemptNumber Number of failed attempt, started from 1 * @param totalAttempts Total request attempts defined for request */ public void attemptFailed(VKRequest request, int attemptNumber, int totalAttempts) { } /** * Called immediately if there was API error, or after <b>attempts</b> tries if there was an HTTP error * * @param error error for VKRequest */ public void onError(VKError error) { } /** * Specify progress for uploading or downloading. Useless for text requests (because gzip encoding bytesTotal will always return -1) * * @param progressType type of progress (upload or download) * @param bytesLoaded total bytes loaded * @param bytesTotal total bytes suppose to be loaded */ public void onProgress(VKProgressType progressType, long bytesLoaded, long bytesTotal) { } } public static VKRequest getRegisteredRequest(long requestId) { return (VKRequest) getRegisteredObject(requestId); } @Override public String toString() { StringBuilder builder = new StringBuilder(super.toString()); builder.append("{").append(methodName).append(" "); VKParameters parameters = getMethodParameters(); for (String key : parameters.keySet()) { builder.append(key).append("=").append(parameters.get(key)).append(" "); } builder.append("}"); return builder.toString(); } }