aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java706
1 files changed, 706 insertions, 0 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java
new file mode 100644
index 000000000..ccb20a764
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java
@@ -0,0 +1,706 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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 org.sufficientlysecure.keychain.ui.linked;
+
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Random;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.app.FragmentActivity;
+import android.util.Base64;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.webkit.CookieManager;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import javax.net.ssl.HttpsURLConnection;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.linked.LinkedAttribute;
+import org.sufficientlysecure.keychain.linked.resources.GithubResource;
+import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.ui.widget.StatusIndicator;
+import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+public class LinkedIdCreateGithubFragment extends CryptoOperationFragment<SaveKeyringParcel,EditKeyResult> {
+
+ public static final String ARG_GITHUB_COOKIE = "github_cookie";
+ private Button mRetryButton;
+
+ enum State {
+ IDLE, AUTH_PROCESS, AUTH_ERROR, POST_PROCESS, POST_ERROR, LID_PROCESS, LID_ERROR, DONE
+ }
+
+ ViewAnimator mButtonContainer;
+
+ StatusIndicator mStatus1, mStatus2, mStatus3;
+
+ byte[] mFingerprint;
+ long mMasterKeyId;
+ private SaveKeyringParcel mSaveKeyringParcel;
+ private TextView mLinkedIdTitle, mLinkedIdComment;
+ private boolean mFinishOnStop;
+
+ public static LinkedIdCreateGithubFragment newInstance() {
+ return new LinkedIdCreateGithubFragment();
+ }
+
+ public LinkedIdCreateGithubFragment() {
+ super(null);
+ }
+
+ @Override @NonNull
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.linked_create_github_fragment, container, false);
+
+ mButtonContainer = (ViewAnimator) view.findViewById(R.id.button_container);
+
+ mStatus1 = (StatusIndicator) view.findViewById(R.id.linked_status_step1);
+ mStatus2 = (StatusIndicator) view.findViewById(R.id.linked_status_step2);
+ mStatus3 = (StatusIndicator) view.findViewById(R.id.linked_status_step3);
+
+ mRetryButton = (Button) view.findViewById(R.id.button_retry);
+
+ ((ImageView) view.findViewById(R.id.linked_id_type_icon)).setImageResource(R.drawable.linked_github);
+ ((ImageView) view.findViewById(R.id.linked_id_certified_icon)).setImageResource(R.drawable.octo_link_24dp);
+ mLinkedIdTitle = (TextView) view.findViewById(R.id.linked_id_title);
+ mLinkedIdComment = (TextView) view.findViewById(R.id.linked_id_comment);
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdWizard activity = (LinkedIdWizard) getActivity();
+ if (activity == null) {
+ return;
+ }
+ activity.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ step1GetOAuthCode();
+ // for animation testing
+ // onCryptoOperationSuccess(null);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
+ mFingerprint = wizard.mFingerprint;
+ mMasterKeyId = wizard.mMasterKeyId;
+ }
+
+ private void step1GetOAuthCode() {
+
+ setState(State.AUTH_PROCESS);
+
+ mButtonContainer.setDisplayedChild(1);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ oAuthRequest("github.com/login/oauth/authorize", BuildConfig.GITHUB_CLIENT_ID, "gist");
+ }
+ }, 300);
+
+ }
+
+ private void showRetryForOAuth() {
+
+ mRetryButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ v.setOnClickListener(null);
+ step1GetOAuthCode();
+ }
+ });
+ mButtonContainer.setDisplayedChild(3);
+
+ }
+
+ private void step1GetOAuthToken() {
+
+ if (mOAuthCode == null) {
+ setState(State.AUTH_ERROR);
+ showRetryForOAuth();
+ return;
+ }
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ final String gistText = GithubResource.generate(activity, mFingerprint);
+
+ new AsyncTask<Void,Void,JSONObject>() {
+
+ Exception mException;
+
+ @Override
+ protected JSONObject doInBackground(Void... dummy) {
+ try {
+
+ JSONObject params = new JSONObject();
+ params.put("client_id", BuildConfig.GITHUB_CLIENT_ID);
+ params.put("client_secret", BuildConfig.GITHUB_CLIENT_SECRET);
+ params.put("code", mOAuthCode);
+ params.put("state", mOAuthState);
+
+ return jsonHttpRequest("https://github.com/login/oauth/access_token", params, null);
+
+ } catch (IOException | HttpResultException e) {
+ mException = e;
+ } catch (JSONException e) {
+ throw new AssertionError("json error, this is a bug!");
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(JSONObject result) {
+ super.onPostExecute(result);
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ // we couldn't show an error anyways
+ return;
+ }
+
+ Log.d(Constants.TAG, "response: " + result);
+
+ if (result == null || result.optString("access_token", null) == null) {
+ setState(State.AUTH_ERROR);
+ showRetryForOAuth();
+
+ if (result != null) {
+ Notify.create(activity, R.string.linked_error_auth_failed, Style.ERROR).show();
+ return;
+ }
+
+ if (mException instanceof SocketTimeoutException) {
+ Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show();
+ } else if (mException instanceof HttpResultException) {
+ Notify.create(activity, activity.getString(R.string.linked_error_http,
+ ((HttpResultException) mException).mResponse),
+ Style.ERROR).show();
+ } else if (mException instanceof IOException) {
+ Notify.create(activity, R.string.linked_error_network, Style.ERROR).show();
+ }
+
+ return;
+ }
+
+ step2PostGist(result.optString("access_token"), gistText);
+
+ }
+ }.execute();
+
+ }
+
+ private void step2PostGist(final String accessToken, final String gistText) {
+
+ setState(State.POST_PROCESS);
+
+ new AsyncTask<Void,Void,JSONObject>() {
+
+ Exception mException;
+
+ @Override
+ protected JSONObject doInBackground(Void... dummy) {
+ try {
+
+ long timer = System.currentTimeMillis();
+
+ JSONObject file = new JSONObject();
+ file.put("content", gistText);
+
+ JSONObject files = new JSONObject();
+ files.put("openpgp.txt", file);
+
+ JSONObject params = new JSONObject();
+ params.put("public", true);
+ params.put("description", getString(R.string.linked_gist_description));
+ params.put("files", files);
+
+ JSONObject result = jsonHttpRequest("https://api.github.com/gists", params, accessToken);
+
+ // ux flow: this operation should take at last a second
+ timer = System.currentTimeMillis() -timer;
+ if (timer < 1000) try {
+ Thread.sleep(1000 -timer);
+ } catch (InterruptedException e) {
+ // never mind
+ }
+
+ return result;
+
+ } catch (IOException | HttpResultException e) {
+ mException = e;
+ } catch (JSONException e) {
+ throw new AssertionError("json error, this is a bug!");
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(JSONObject result) {
+ super.onPostExecute(result);
+
+ Log.d(Constants.TAG, "response: " + result);
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ // we couldn't show an error anyways
+ return;
+ }
+
+ if (result == null) {
+ setState(State.POST_ERROR);
+ showRetryForOAuth();
+
+ if (mException instanceof SocketTimeoutException) {
+ Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show();
+ } else if (mException instanceof HttpResultException) {
+ Notify.create(activity, activity.getString(R.string.linked_error_http,
+ ((HttpResultException) mException).mResponse),
+ Style.ERROR).show();
+ } else if (mException instanceof IOException) {
+ Notify.create(activity, R.string.linked_error_network, Style.ERROR).show();
+ }
+
+ return;
+ }
+
+ GithubResource resource;
+
+ try {
+ String gistId = result.getString("id");
+ JSONObject owner = result.getJSONObject("owner");
+ String gistLogin = owner.getString("login");
+
+ URI uri = URI.create("https://gist.github.com/" + gistLogin + "/" + gistId);
+ resource = GithubResource.create(uri);
+ } catch (JSONException e) {
+ setState(State.POST_ERROR);
+ return;
+ }
+
+ View linkedItem = mButtonContainer.getChildAt(2);
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ linkedItem.setTransitionName(resource.toUri().toString());
+ }
+
+ // we only need authorization for this one operation, drop it afterwards
+ revokeToken(accessToken);
+
+ step3EditKey(resource);
+ }
+
+ }.execute();
+
+ }
+
+ private void revokeToken(final String token) {
+
+ new AsyncTask<Void,Void,Void>() {
+ @Override
+ protected Void doInBackground(Void... dummy) {
+ try {
+ HttpsURLConnection nection = (HttpsURLConnection) new URL(
+ "https://api.github.com/applications/" + BuildConfig.GITHUB_CLIENT_ID + "/tokens/" + token)
+ .openConnection();
+ nection.setRequestMethod("DELETE");
+ String encoded = Base64.encodeToString(
+ (BuildConfig.GITHUB_CLIENT_ID + ":" + BuildConfig.GITHUB_CLIENT_SECRET).getBytes(), Base64.DEFAULT);
+ nection.setRequestProperty("Authorization", "Basic " + encoded);
+ nection.connect();
+ } catch (IOException e) {
+ // nvm
+ }
+ return null;
+ }
+ }.execute();
+
+ }
+
+ private void step3EditKey(final GithubResource resource) {
+
+ // set item data while we're there
+ {
+ Context context = getActivity();
+ mLinkedIdTitle.setText(resource.getDisplayTitle(context));
+ mLinkedIdComment.setText(resource.getDisplayComment(context));
+ }
+
+ setState(State.LID_PROCESS);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+
+ WrappedUserAttribute ua = LinkedAttribute.fromResource(resource).toUserAttribute();
+ mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint);
+ mSaveKeyringParcel.mAddUserAttribute.add(ua);
+
+ cryptoOperation();
+
+ }
+ }, 250);
+
+ }
+
+ @Nullable
+ @Override
+ public SaveKeyringParcel createOperationInput() {
+ // if this is null, the cryptoOperation silently aborts - which is what we want in that case
+ return mSaveKeyringParcel;
+ }
+
+ @Override
+ public void onCryptoOperationSuccess(EditKeyResult result) {
+
+ setState(State.DONE);
+
+ mButtonContainer.getInAnimation().setDuration(750);
+ mButtonContainer.setDisplayedChild(2);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ FragmentActivity activity = getActivity();
+ Intent intent = new Intent(activity, ViewKeyActivity.class);
+ intent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId));
+ // intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ intent.putExtra(ViewKeyActivity.EXTRA_LINKED_TRANSITION, true);
+ View linkedItem = mButtonContainer.getChildAt(2);
+
+ Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(
+ activity, linkedItem, linkedItem.getTransitionName()).toBundle();
+ activity.startActivity(intent, options);
+ mFinishOnStop = true;
+ } else {
+ activity.startActivity(intent);
+ activity.finish();
+ }
+ }
+ }, 1000);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ // cookies are automatically saved, we don't want that
+ CookieManager cookieManager = CookieManager.getInstance();
+ String cookie = cookieManager.getCookie("https://github.com/");
+ outState.putString(ARG_GITHUB_COOKIE, cookie);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ if (savedInstanceState != null) {
+ String cookie = savedInstanceState.getString(ARG_GITHUB_COOKIE);
+ CookieManager cookieManager = CookieManager.getInstance();
+ cookieManager.setCookie("https://github.com/", cookie);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ try {
+ // cookies are automatically saved, we don't want that
+ CookieManager cookieManager = CookieManager.getInstance();
+ // noinspection deprecation (replacement is api lvl 21)
+ cookieManager.removeAllCookie();
+ } catch (Exception e) {
+ // no biggie if this fails
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ if (mFinishOnStop) {
+ Activity activity = getActivity();
+ activity.setResult(Activity.RESULT_OK);
+ activity.finish();
+ }
+ }
+
+ @Override
+ public void onCryptoOperationError(EditKeyResult result) {
+ result.createNotify(getActivity()).show(this);
+ setState(State.LID_ERROR);
+ }
+
+ @Override
+ public void onCryptoOperationCancelled() {
+ mRetryButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ v.setOnClickListener(null);
+ mButtonContainer.setDisplayedChild(1);
+ setState(State.LID_PROCESS);
+ cryptoOperation();
+ }
+ });
+ mButtonContainer.setDisplayedChild(3);
+ setState(State.LID_ERROR);
+ }
+
+ private String mOAuthCode, mOAuthState;
+
+ public void oAuthRequest(String hostAndPath, String clientId, String scope) {
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ byte[] buf = new byte[16];
+ new Random().nextBytes(buf);
+ mOAuthState = new String(Hex.encode(buf));
+ mOAuthCode = null;
+
+ final Dialog auth_dialog = new Dialog(activity);
+ auth_dialog.setContentView(R.layout.oauth_webview);
+ WebView web = (WebView) auth_dialog.findViewById(R.id.web_view);
+ web.getSettings().setSaveFormData(false);
+ web.getSettings().setUserAgentString("OpenKeychain " + BuildConfig.VERSION_NAME);
+ web.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ Uri uri = Uri.parse(url);
+ if ("oauth-openkeychain".equals(uri.getScheme())) {
+
+ if (mOAuthCode != null) {
+ return true;
+ }
+
+ if (uri.getQueryParameter("error") != null) {
+ Log.i(Constants.TAG, "got oauth error: " + uri.getQueryParameter("error"));
+ auth_dialog.dismiss();
+ return true;
+ }
+
+ // check if mOAuthState == queryParam[state]
+ mOAuthCode = uri.getQueryParameter("code");
+
+ auth_dialog.dismiss();
+ return true;
+ }
+ // don't surf away from github!
+ if (!"github.com".equals(uri.getHost())) {
+ auth_dialog.dismiss();
+ return true;
+ }
+ return false;
+ }
+
+ });
+
+ auth_dialog.setTitle(R.string.linked_webview_title_github);
+ auth_dialog.setCancelable(true);
+ auth_dialog.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ step1GetOAuthToken();
+ }
+ });
+ auth_dialog.show();
+
+ web.loadUrl("https://" + hostAndPath +
+ "?client_id=" + clientId +
+ "&scope=" + scope +
+ "&redirect_uri=oauth-openkeychain://linked/" +
+ "&state=" + mOAuthState);
+
+ }
+
+ public void setState(State state) {
+ switch (state) {
+ case IDLE:
+ mStatus1.setDisplayedChild(Status.IDLE);
+ mStatus2.setDisplayedChild(Status.IDLE);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case AUTH_PROCESS:
+ mStatus1.setDisplayedChild(Status.PROGRESS);
+ mStatus2.setDisplayedChild(Status.IDLE);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case AUTH_ERROR:
+ mStatus1.setDisplayedChild(Status.ERROR);
+ mStatus2.setDisplayedChild(Status.IDLE);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case POST_PROCESS:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.PROGRESS);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case POST_ERROR:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.ERROR);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case LID_PROCESS:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.OK);
+ mStatus3.setDisplayedChild(Status.PROGRESS);
+ break;
+ case LID_ERROR:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.OK);
+ mStatus3.setDisplayedChild(Status.ERROR);
+ break;
+ case DONE:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.OK);
+ mStatus3.setDisplayedChild(Status.OK);
+ }
+ }
+
+ private static JSONObject jsonHttpRequest(String url, JSONObject params, String accessToken)
+ throws IOException, HttpResultException {
+
+ HttpsURLConnection nection = (HttpsURLConnection) new URL(url).openConnection();
+ nection.setDoInput(true);
+ nection.setDoOutput(true);
+ nection.setConnectTimeout(2000);
+ nection.setReadTimeout(1000);
+ nection.setRequestProperty("Content-Type", "application/json");
+ nection.setRequestProperty("Accept", "application/json");
+ nection.setRequestProperty("User-Agent", "OpenKeychain " + BuildConfig.VERSION_NAME);
+ if (accessToken != null) {
+ nection.setRequestProperty("Authorization", "token " + accessToken);
+ }
+
+ OutputStream os = nection.getOutputStream();
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+ writer.write(params.toString());
+ writer.flush();
+ writer.close();
+ os.close();
+
+ try {
+
+ nection.connect();
+
+ int code = nection.getResponseCode();
+ if (code != HttpsURLConnection.HTTP_CREATED && code != HttpsURLConnection.HTTP_OK) {
+ throw new HttpResultException(nection.getResponseCode(), nection.getResponseMessage());
+ }
+
+ InputStream in = new BufferedInputStream(nection.getInputStream());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ StringBuilder response = new StringBuilder();
+ while (true) {
+ String line = reader.readLine();
+ if (line == null) {
+ break;
+ }
+ response.append(line);
+ }
+
+ try {
+ return new JSONObject(response.toString());
+ } catch (JSONException e) {
+ throw new IOException(e);
+ }
+
+ } finally {
+ nection.disconnect();
+ }
+
+ }
+
+ static class HttpResultException extends Exception {
+ final int mCode;
+ final String mResponse;
+
+ HttpResultException(int code, String response) {
+ mCode = code;
+ mResponse = response;
+ }
+
+ }
+
+}