mInventorySet = new HashSet<>();
+ private GridView mGridView;
+ private ProgressBar mProgressBar;
+ private TextView mError;
+ private IabHelper mHelper;
+ private final IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener =
+ new IabHelper.OnIabPurchaseFinishedListener() {
+ public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
+ if (mHelper == null) return;
+ if (result.isFailure()) {
+ complain("Error purchasing: " + result);
+ setWaitScreen(false);
+ return;
+ }
+
+ if (!verifyDeveloperPayload(purchase)) {
+ complain("Error purchasing. Authenticity verification failed.");
+ setWaitScreen(false);
+ return;
+ }
+
+ // else, it is a success, the user has donated!
+ String sku = purchase.getSku();
+ mInventorySet.add(sku);
+ PreferenceHelper.setHasDonated(getActivity(), true);
+ }
+ };
+ private Donation[] mDonationList;
+ private final IabHelper.QueryInventoryFinishedListener mGotInventoryListener =
+ new IabHelper.QueryInventoryFinishedListener() {
+ public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
+ if (mHelper == null) return;
+ if (result.isFailure()) {
+ complain("Failed to query inventory: " + result);
+ return;
+ }
+
+ mInventorySet.clear();
+ for (Donation donation : mDonationList) {
+ Purchase purchase = inventory.getPurchase(donation.sku);
+ boolean isBought = (purchase != null && verifyDeveloperPayload(purchase));
+
+ if (isBought) {
+ mInventorySet.add(donation.sku);
+ PreferenceHelper.setHasDonated(getActivity(), true);
+ }
+ }
+
+ /*
+ // Fake items to debug user interface.
+ mInventorySet.add(mDonationList[0].sku);
+ mInventorySet.add(mDonationList[1].sku);
+ mInventorySet.add(mDonationList[2].sku);
+ */
+
+ updateUi();
+ setWaitScreen(false);
+ }
+ };
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mDonationList = DonationItems.get(getResources());
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ assert activity != null;
+
+ View view = new DialogHelper.Builder(activity)
+ .setTitle(R.string.donation_title)
+ .setView(R.layout.donation_dialog)
+ .createSkeletonView();
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+ .setView(view)
+ .setNegativeButton(android.R.string.cancel, null);
+
+ TextView info = (TextView) view.findViewById(R.id.info);
+ info.setText(Html.fromHtml(getString(R.string.donation_info)));
+ info.setMovementMethod(new LinkMovementMethod());
+
+ mError = (TextView) view.findViewById(R.id.error);
+ mProgressBar = (ProgressBar) view.findViewById(android.R.id.progress);
+ mGridView = (GridView) view.findViewById(R.id.grid);
+ mGridView.setAdapter(new DonationAdapter(getActivity(), mDonationList, mInventorySet));
+ mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ DonationAdapter adapter = (DonationAdapter) parent.getAdapter();
+ Donation donation = adapter.getItem(position);
+
+ if (!mInventorySet.contains(donation.sku)) {
+ /**
+ * See {@link sharedcode.turboeditor.iab.DonationFragment#verifyDeveloperPayload(Purchase)}.
+ */
+ String payload = "";
+ try {
+ mHelper.launchPurchaseFlow(
+ getActivity(), donation.sku, RC_REQUEST,
+ mPurchaseFinishedListener, payload);
+ } catch (Exception e) {
+ ToastUtils.showShort(getActivity(), "Failed to launch a purchase flow.");
+ }
+ } else {
+ ToastUtils.showShort(getActivity(), getString(R.string.donation_item_bought));
+ }
+ }
+ });
+
+ final AlertDialog alertDialog;
+
+ // Show PayPal button.
+ final Intent paypalIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(Build.Links.DONATE));
+ builder.setNeutralButton(R.string.paypal, null);
+
+ alertDialog = builder.create();
+ alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
+
+ @Override
+ public void onShow(DialogInterface dialog) {
+ Data[] datas = new Data[]{
+ new Data(
+ alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL),
+ paypalIntent, R.drawable.ic_action_paypal)
+ };
+
+ ImageSpan span;
+ SpannableString text;
+ for (final Data data : datas) {
+ final Button btn = data.button;
+ if (btn != null) {
+ span = new ImageSpan(getActivity(), data.iconResource);
+
+ // Replace text with an icon.
+ // This is a workaround to fix compound button's aligment.
+ text = new SpannableString(" ");
+ text.setSpan(span, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ btn.setText(text);
+
+ // Eat default weight.
+ btn.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ btn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startPaymentIntentWithWarningAlertDialog(data.intent);
+ }
+ });
+ }
+ }
+ }
+
+ final class Data {
+
+ private final Button button;
+ private final Intent intent;
+ private final int iconResource;
+
+ private Data(Button button, Intent intent, int iconResource) {
+ this.button = button;
+ this.intent = intent;
+ this.iconResource = iconResource;
+ }
+ }
+ });
+
+ initBilling();
+
+ return alertDialog;
+ }
+
+ /**
+ * Shows a warning alert dialog to note, that those methods
+ * may suck hard and nobody will care about it.
+ * Starts an intent if user is agree with it.
+ */
+ private void startPaymentIntentWithWarningAlertDialog(final Intent intent) {
+ CharSequence messageText = getString(R.string.donation_no_responsibility);
+ new DialogHelper.Builder(getActivity())
+ .setMessage(messageText)
+ .wrap()
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ startActivity(intent);
+ dismiss(); // Dismiss main fragment
+ } catch (ActivityNotFoundException e) { /* hell no */ }
+ }
+ })
+ .create()
+ .show();
+ }
+
+ private void setWaitScreen(boolean loading) {
+ ViewUtils.setVisible(mProgressBar, loading);
+ ViewUtils.setVisible(mGridView, !loading);
+ ViewUtils.setVisible(mError, false);
+ }
+
+ private void setErrorScreen(String errorMessage, final Runnable runnable) {
+ mProgressBar.setVisibility(View.GONE);
+ mGridView.setVisibility(View.GONE);
+ mError.setVisibility(View.VISIBLE);
+ mError.setText(errorMessage);
+ mError.setOnClickListener(runnable != null ? new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ runnable.run();
+ }
+ } : null);
+ }
+
+ /**
+ * Updates GUI to display changes.
+ */
+ private void updateUi() {
+ DonationAdapter adapter = (DonationAdapter) mGridView.getAdapter();
+ adapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ disposeBilling();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (mHelper.handleActivityResult(requestCode, resultCode, data)) {
+ return;
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ /**
+ * Releases billing service.
+ *
+ * @see #initBilling()
+ */
+ private void disposeBilling() {
+ if (mHelper != null) {
+ mHelper.dispose();
+ mHelper = null;
+ }
+ }
+
+ /**
+ * Make sure you call {@link #disposeBilling()}!
+ *
+ * @see #disposeBilling()
+ */
+ private void initBilling() {
+ setWaitScreen(true);
+ disposeBilling();
+
+ String base64EncodedPublicKey = Build.GOOGLE_PLAY_PUBLIC_KEY;
+ mHelper = new IabHelper(getActivity(), base64EncodedPublicKey);
+ mHelper.enableDebugLogging(Build.DEBUG);
+ mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
+ public void onIabSetupFinished(IabResult result) {
+ if (mHelper == null) return;
+ if (!result.isSuccess()) {
+ setErrorScreen(getString(R.string.donation_error_iab_setup), new Runnable() {
+ @Override
+ public void run() {
+ // Try to initialize billings again.
+ initBilling();
+ }
+ });
+ return;
+ }
+
+ setWaitScreen(false);
+ mHelper.queryInventoryAsync(mGotInventoryListener);
+ }
+ });
+ }
+
+ private boolean verifyDeveloperPayload(Purchase purchase) {
+ // TODO: This method itself is a big question.
+ // Personally, I think that this whole ‘best practices’ part
+ // is confusing and is trying to make you do work that the API
+ // should really be doing. Since the purchase is tied to a Google account,
+ // and the Play Store obviously saves this information, they should
+ // just give you this in the purchase details. Getting a proper user ID
+ // requires additional permissions that you shouldn’t need to add just
+ // to cover for the deficiencies of the IAB API.
+ return true;
+ }
+
+ private void complain(String message) {
+ ToastUtils.showShort(getActivity(), message);
+ }
+
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/DonationItems.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/DonationItems.java
new file mode 100644
index 0000000..34a3db1
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/DonationItems.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab;
+
+import android.content.res.Resources;
+
+import sharedcode.turboeditor.R;
+
+/**
+ * Created by achep on 07.05.14 for AcDisplay.
+ *
+ * @author Artem Chepurnoy
+ */
+public class DonationItems {
+
+ public static Donation[] get(Resources res) {
+ int[] data = new int[]{
+ 2, R.string.donation_2,
+ 4, R.string.donation_4,
+ 10, R.string.donation_10,
+ 20, R.string.donation_20,
+ 50, R.string.donation_50,
+ 99, R.string.donation_99,
+ };
+
+ Donation[] donation = new Donation[data.length / 2];
+
+ int length = donation.length;
+ for (int i = 0; i < length; i++) {
+ donation[i] = new Donation(data[i * 2],
+ res.getString(data[i * 2 + 1]));
+ }
+ return donation;
+ }
+
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Base64.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Base64.java
new file mode 100644
index 0000000..94b7d50
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Base64.java
@@ -0,0 +1,582 @@
+// Portions copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sharedcode.turboeditor.iab.utils;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * http://iharder.net/xmlizable
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rharder@usa.net
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder;
+ * it simply converts binary data to base64 data and back.
+ *
+ * Note {@link CharBase64} is a GWT-compatible implementation of this
+ * class.
+ */
+public class Base64 {
+ /**
+ * Specify encoding (value is {@code true}).
+ */
+ public final static boolean ENCODE = true;
+
+ /**
+ * Specify decoding (value is {@code false}).
+ */
+ public final static boolean DECODE = false;
+
+ /**
+ * The equals sign (=) as a byte.
+ */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /**
+ * The new line character (\n) as a byte.
+ */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '+', (byte) '/'};
+
+ /**
+ * The 64 valid web safe Base64 values.
+ */
+ private final static byte[] WEBSAFE_ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '-', (byte) '_'};
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ */
+ private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ /**
+ * The web safe decodabet
+ */
+ private final static byte[] WEBSAFE_DECODABET =
+ {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+ 62, // Dash '-' sign at decimal 45
+ -9, -9, // Decimal 46-47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, // Decimal 91-94
+ 63, // Underscore '_' at decimal 95
+ -9, // Decimal 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ // Indicates white space in encoding
+ private final static byte WHITE_SPACE_ENC = -5;
+ // Indicates equals sign in encoding
+ private final static byte EQUALS_SIGN_ENC = -1;
+
+ /**
+ * Defeats instantiation.
+ */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param alphabet is the encoding alphabet
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset,
+ int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index alphabet
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff =
+ (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+ return destination;
+ case 2:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ case 1:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Equivalent to calling
+ * {@code encodeBytes(source, 0, source.length)}
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encode(byte[] source) {
+ return encode(source, 0, source.length, ALPHABET, true);
+ }
+
+ /**
+ * Encodes a byte array into web safe Base64 notation.
+ *
+ * @param source The data to convert
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ */
+ public static String encodeWebSafe(byte[] source, boolean doPadding) {
+ return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet the encoding alphabet
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ * @since 1.4
+ */
+ public static String encode(byte[] source, int off, int len, byte[] alphabet,
+ boolean doPadding) {
+ byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+ int outLen = outBuff.length;
+
+ // If doPadding is false, set length to truncate '='
+ // padding characters
+ while (doPadding == false && outLen > 0) {
+ if (outBuff[outLen - 1] != '=') {
+ break;
+ }
+ outLen -= 1;
+ }
+
+ return new String(outBuff, 0, outLen);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet is the encoding alphabet
+ * @param maxLineLength maximum length of one line.
+ * @return the BASE64-encoded byte array
+ */
+ public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+ int maxLineLength) {
+ int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+ int len43 = lenDiv3 * 4;
+ byte[] outBuff = new byte[len43 // Main 4:3
+ + (len43 / maxLineLength)]; // New lines
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+
+ // The following block of code is the same as
+ // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+ // but inlined for faster encoding (~20% improvement)
+ int inBuff =
+ ((source[d + off] << 24) >>> 8)
+ | ((source[d + 1 + off] << 24) >>> 16)
+ | ((source[d + 2 + off] << 24) >>> 24);
+ outBuff[e] = alphabet[(inBuff >>> 18)];
+ outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // end for: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ // Add a last newline
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ }
+ e += 4;
+ }
+
+ assert (e == outBuff.length);
+ return outBuff;
+ }
+
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset,
+ byte[] destination, int destOffset, byte[] decodabet) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Example: DkL=
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ } else {
+ // Example: DkLE
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+ | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+ return 3;
+ }
+ } // end decodeToBytes
+
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decode(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes data from web safe Base64 notation.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decodeWebSafe(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @throws Base64DecoderException
+ * @since 1.3
+ */
+ public static byte[] decode(byte[] source) throws Base64DecoderException {
+ return decode(source, 0, source.length);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded data.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source)
+ throws Base64DecoderException {
+ return decodeWebSafe(source, 0, source.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ * @throws Base64DecoderException
+ * @since 1.3
+ */
+ public static byte[] decode(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, DECODABET);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded byte array.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, WEBSAFE_DECODABET);
+ }
+
+ /**
+ * Decodes Base64 content using the supplied decodabet and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return decoded data
+ */
+ public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+ throws Base64DecoderException {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = 0; i < len; i++) {
+ sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+ sbiDecode = decodabet[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ // An equals sign (for padding) must not occur at position 0 or 1
+ // and must be the last byte[s] in the encoded value
+ if (sbiCrop == EQUALS_SIGN) {
+ int bytesLeft = len - i;
+ byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+ if (b4Posn == 0 || b4Posn == 1) {
+ throw new Base64DecoderException(
+ "invalid padding byte '=' at byte offset " + i);
+ } else if ((b4Posn == 3 && bytesLeft > 2)
+ || (b4Posn == 4 && bytesLeft > 1)) {
+ throw new Base64DecoderException(
+ "padding byte '=' falsely signals end of encoded value "
+ + "at offset " + i
+ );
+ } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+ throw new Base64DecoderException(
+ "encoded value has invalid trailing byte");
+ }
+ break;
+ }
+
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn == 4) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ b4Posn = 0;
+ }
+ }
+ } else {
+ throw new Base64DecoderException("Bad Base64 input character at " + i
+ + ": " + source[i + off] + "(decimal)");
+ }
+ }
+
+ // Because web safe encoding allows non padding base64 encodes, we
+ // need to pad the rest of the b4 buffer with equal signs when
+ // b4Posn != 0. There can be at most 2 equal signs at the end of
+ // four characters, so the b4 buffer must have two or three
+ // characters. This also catches the case where the input is
+ // padded with EQUALS_SIGN
+ if (b4Posn != 0) {
+ if (b4Posn == 1) {
+ throw new Base64DecoderException("single trailing character at offset "
+ + (len - 1));
+ }
+ b4[b4Posn++] = EQUALS_SIGN;
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ }
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ApiHelper.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Base64DecoderException.java
similarity index 66%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ApiHelper.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Base64DecoderException.java
index df7e17e..8f4c024 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ApiHelper.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Base64DecoderException.java
@@ -17,12 +17,21 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.iab.utils;
-import android.os.Build;
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+ private static final long serialVersionUID = 1L;
-public class ApiHelper {
- public static boolean is11(){
- return Build.VERSION.SDK_INT >= 11;
+ public Base64DecoderException() {
+ super();
+ }
+
+ public Base64DecoderException(String s) {
+ super(s);
}
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabException.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabException.java
new file mode 100644
index 0000000..2e00564
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabException.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+ IabResult mResult;
+
+ public IabException(IabResult r) {
+ this(r, null);
+ }
+
+ public IabException(int response, String message) {
+ this(new IabResult(response, message));
+ }
+
+ public IabException(IabResult r, Exception cause) {
+ super(r.getMessage(), cause);
+ mResult = r;
+ }
+
+ public IabException(int response, String message, Exception cause) {
+ this(new IabResult(response, message), cause);
+ }
+
+ /**
+ * Returns the IAB result (error) that this exception signals.
+ */
+ public IabResult getResult() {
+ return mResult;
+ }
+}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabHelper.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabHelper.java
new file mode 100644
index 0000000..61060f0
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabHelper.java
@@ -0,0 +1,966 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+import org.json.JSONException;
+import com.android.vending.billing.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ * @author Bruno Oliveira (Google)
+ */
+public class IabHelper {
+ // Billing response codes
+ public static final int BILLING_RESPONSE_RESULT_OK = 0;
+ public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+ public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+ public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+ public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+ // IAB Helper error codes
+ public static final int IABHELPER_ERROR_BASE = -1000;
+ public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+ public static final int IABHELPER_BAD_RESPONSE = -1002;
+ public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+ public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+ public static final int IABHELPER_USER_CANCELLED = -1005;
+ public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+ public static final int IABHELPER_MISSING_TOKEN = -1007;
+ public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+ public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+ public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+ // Keys for the responses from InAppBillingService
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+ public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+ public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+ public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+ public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+ public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+ // Item types
+ public static final String ITEM_TYPE_INAPP = "inapp";
+ public static final String ITEM_TYPE_SUBS = "subs";
+ // some fields on the getSkuDetails response bundle
+ public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+ public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+ // Is debug logging enabled?
+ boolean mDebugLog = false;
+ String mDebugTag = "IabHelper";
+ // Is setup done?
+ boolean mSetupDone = false;
+ // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+ boolean mDisposed = false;
+ boolean mIsBound = false;
+ // Are subscriptions supported?
+ boolean mSubscriptionsSupported = false;
+ // Is an asynchronous operation in progress?
+ // (only one at a time can be in progress)
+ boolean mAsyncInProgress = false;
+ // (for logging/debugging)
+ // if mAsyncInProgress == true, what asynchronous operation is in progress?
+ String mAsyncOperation = "";
+ // Context we were passed during initialization
+ Context mContext;
+ // Connection to the service
+ IInAppBillingService mService;
+ ServiceConnection mServiceConn;
+ // The request code used to launch purchase flow
+ int mRequestCode;
+ // The item type of the current purchase flow
+ String mPurchasingItemType;
+ // Public key for verifying signature, in base64 encoding
+ String mSignatureBase64 = null;
+ // The listener registered on launchPurchaseFlow, which we have to call back when
+ // the purchase finishes
+ OnIabPurchaseFinishedListener mPurchaseListener;
+
+ /**
+ * Creates an instance. After creation, it will not yet be ready to use. You must perform
+ * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+ * block and is safe to call from a UI thread.
+ *
+ * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+ * @param base64PublicKey Your application's public key, encoded in base64.
+ * This is used for verification of purchase signatures. You can find your app's base64-encoded
+ * public key in your application's page on Google Play Developer Console. Note that this
+ * is NOT your "developer public key".
+ */
+ public IabHelper(Context ctx, String base64PublicKey) {
+ mContext = ctx.getApplicationContext();
+ mSignatureBase64 = base64PublicKey;
+ logDebug("IAB helper created.");
+ }
+
+ /**
+ * Returns a human-readable description for the given response code.
+ *
+ * @param code The response code
+ * @return A human-readable string explaining the result code.
+ * It also includes the result code numerically.
+ */
+ public static String getResponseDesc(int code) {
+ String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+ "3:Billing Unavailable/4:Item unavailable/" +
+ "5:Developer Error/6:Error/7:Item Already Owned/" +
+ "8:Item not owned").split("/");
+ String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+ "-1002:Bad response received/" +
+ "-1003:Purchase signature verification failed/" +
+ "-1004:Send intent failed/" +
+ "-1005:User cancelled/" +
+ "-1006:Unknown purchase response/" +
+ "-1007:Missing token/" +
+ "-1008:Unknown error/" +
+ "-1009:Subscriptions not available/" +
+ "-1010:Invalid consumption attempt").split("/");
+
+ if (code <= IABHELPER_ERROR_BASE) {
+ int index = IABHELPER_ERROR_BASE - code;
+ if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+ else return String.valueOf(code) + ":Unknown IAB Helper Error";
+ } else if (code < 0 || code >= iab_msgs.length)
+ return String.valueOf(code) + ":Unknown";
+ else
+ return iab_msgs[code];
+ }
+
+ /**
+ * Enables or disable debug logging through LogCat.
+ */
+ public void enableDebugLogging(boolean enable, String tag) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ mDebugTag = tag;
+ }
+
+ public void enableDebugLogging(boolean enable) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ }
+
+ /**
+ * Starts the setup process. This will start up the setup process asynchronously.
+ * You will be notified through the listener when the setup process is complete.
+ * This method is safe to call from a UI thread.
+ *
+ * @param listener The listener to notify when the setup process is complete.
+ */
+ public void startSetup(final OnIabSetupFinishedListener listener) {
+ // If already set up, can't do it again.
+ checkNotDisposed();
+ if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+ // Connection to IAB service
+ logDebug("Starting in-app billing setup.");
+ mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ logDebug("Billing service disconnected.");
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (mDisposed) return;
+ logDebug("Billing service connected.");
+ mService = IInAppBillingService.Stub.asInterface(service);
+ String packageName = mContext.getPackageName();
+ try {
+ logDebug("Checking for in-app billing 3 support.");
+
+ // check for in-app billing v3 support
+ int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+ "Error checking for billing v3 support."));
+
+ // if in-app purchases aren't supported, neither are subscriptions.
+ mSubscriptionsSupported = false;
+ return;
+ }
+ logDebug("In-app billing version 3 supported for " + packageName);
+
+ // check for v3 subscriptions support
+ response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscriptions AVAILABLE.");
+ mSubscriptionsSupported = true;
+ } else {
+ logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+ }
+
+ mSetupDone = true;
+ } catch (RemoteException e) {
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+ "RemoteException while setting up in-app billing."));
+ }
+ e.printStackTrace();
+ return;
+ }
+
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+ }
+ }
+ };
+
+ Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+ serviceIntent.setPackage("com.android.vending");
+ List ri = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
+ if (ri != null && !ri.isEmpty()) {
+ // service available to handle that Intent
+ mIsBound = mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ } else {
+ // no service available to handle that Intent
+ if (listener != null) {
+ listener.onIabSetupFinished(
+ new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+ "Billing service unavailable on device.")
+ );
+ }
+ }
+ }
+
+ /**
+ * Dispose of object, releasing resources. It's very important to call this
+ * method when you are done with this object. It will release any resources
+ * used by it such as service connections. Naturally, once the object is
+ * disposed of, it can't be used again.
+ */
+ public void dispose() {
+ logDebug("Disposing.");
+ mSetupDone = false;
+ if (mServiceConn != null) {
+ logDebug("Unbinding from service.");
+ if (mContext != null && mIsBound) {
+ mContext.unbindService(mServiceConn);
+ }
+ }
+ mDisposed = true;
+ mContext = null;
+ mServiceConn = null;
+ mService = null;
+ mPurchaseListener = null;
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed)
+ throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+ }
+
+ /**
+ * Returns whether subscriptions are supported.
+ */
+ public boolean subscriptionsSupported() {
+ checkNotDisposed();
+ return mSubscriptionsSupported;
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
+ launchPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener) {
+ launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
+ }
+
+ /**
+ * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+ * which will involve bringing up the Google Play screen. The calling activity will be paused while
+ * the user interacts with Google Play, and the result will be delivered via the activity's
+ * {@link android.app.Activity#onActivityResult} method, at which point you must call
+ * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param act The calling activity.
+ * @param sku The sku of the item to purchase.
+ * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
+ * @param requestCode A request code (to differentiate from other responses --
+ * as in {@link android.app.Activity#startActivityForResult}).
+ * @param listener The listener to notify when the purchase process finishes
+ * @param extraData Extra data (developer payload), which will be returned with the purchase data
+ * when the purchase completes. This extra data will be permanently bound to that purchase
+ * and will always be returned when the purchase is queried.
+ */
+ public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ checkNotDisposed();
+ checkSetupDone("launchPurchaseFlow");
+ flagStartAsync("launchPurchaseFlow");
+ IabResult result;
+
+ if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+ "Subscriptions are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+
+ try {
+ logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+ Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
+ int response = getResponseCodeFromBundle(buyIntentBundle);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logError("Unable to buy item, Error response: " + getResponseDesc(response));
+ flagEndAsync();
+ result = new IabResult(response, "Unable to buy item");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ return;
+ }
+
+ PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+ logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+ mRequestCode = requestCode;
+ mPurchaseListener = listener;
+ mPurchasingItemType = itemType;
+ act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+ requestCode, new Intent(),
+ 0, 0,
+ 0);
+ } catch (SendIntentException e) {
+ logError("SendIntentException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ } catch (RemoteException e) {
+ logError("RemoteException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ }
+
+ /**
+ * Handles an activity result that's part of the purchase flow in in-app billing. If you
+ * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+ * Activity's {@link android.app.Activity@onActivityResult} method. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param requestCode The requestCode as you received it.
+ * @param resultCode The resultCode as you received it.
+ * @param data The data (Intent) as you received it.
+ * @return Returns true if the result was related to a purchase flow and was handled;
+ * false if the result was not related to a purchase, in which case you should
+ * handle it normally.
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ IabResult result;
+ if (requestCode != mRequestCode) return false;
+
+ checkNotDisposed();
+ checkSetupDone("handleActivityResult");
+
+ // end of async purchase operation that started on launchPurchaseFlow
+ flagEndAsync();
+
+ if (data == null) {
+ logError("Null data in IAB activity result.");
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ int responseCode = getResponseCodeFromIntent(data);
+ String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+ String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+ if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successful resultcode from purchase activity.");
+ logDebug("Purchase data: " + purchaseData);
+ logDebug("Data signature: " + dataSignature);
+ logDebug("Extras: " + data.getExtras());
+ logDebug("Expected item type: " + mPurchasingItemType);
+
+ if (purchaseData == null || dataSignature == null) {
+ logError("BUG: either purchaseData or dataSignature is null.");
+ logDebug("Extras: " + data.getExtras().toString());
+ result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ @SuppressWarnings("UnusedAssignment") Purchase purchase = null;
+ try {
+ purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+ String sku = purchase.getSku();
+
+ // Verify signature
+ if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+ logError("Purchase signature verification FAILED for sku " + sku);
+ result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, purchase);
+ return true;
+ }
+ logDebug("Purchase signature successfully verified.");
+ } catch (JSONException e) {
+ logError("Failed to parse purchase data.");
+ e.printStackTrace();
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ if (mPurchaseListener != null) {
+ mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+ }
+ } else if (resultCode == Activity.RESULT_OK) {
+ // result code was OK, but in-app billing response was not OK.
+ logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+ if (mPurchaseListener != null) {
+ result = new IabResult(responseCode, "Problem purchashing item.");
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ } else {
+ logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ + ". Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ return true;
+ }
+
+ public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException {
+ return queryInventory(querySkuDetails, moreSkus, null);
+ }
+
+ /**
+ * Queries the inventory. This will query all owned items from the server, as well as
+ * information on additional skus, if specified. This method may block or take long to execute.
+ * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
+ *
+ * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+ * as purchase information.
+ * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @throws IabException if a problem occurs while refreshing the inventory.
+ */
+ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
+ List moreSubsSkus) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ try {
+ Inventory inv = new Inventory();
+ int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned items).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+ }
+ }
+
+ // if subscriptions are supported, then also query for subscriptions
+ if (mSubscriptionsSupported) {
+ r = queryPurchases(inv, ITEM_TYPE_SUBS);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+ }
+ }
+ }
+
+ return inv;
+ } catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+ } catch (JSONException e) {
+ throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+ }
+ }
+
+ /**
+ * Asynchronous wrapper for inventory query. This will perform an inventory
+ * query as described in {@link #queryInventory}, but will do so asynchronously
+ * and call back the specified listener upon completion. This method is safe to
+ * call from a UI thread.
+ *
+ * @param querySkuDetails as in {@link #queryInventory}
+ * @param moreSkus as in {@link #queryInventory}
+ * @param listener The listener to notify when the refresh operation completes.
+ */
+ public void queryInventoryAsync(final boolean querySkuDetails,
+ final List moreSkus,
+ final QueryInventoryFinishedListener listener) {
+ final Handler handler = new Handler();
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ flagStartAsync("refresh inventory");
+ (new Thread(new Runnable() {
+ public void run() {
+ IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+ Inventory inv = null;
+ try {
+ inv = queryInventory(querySkuDetails, moreSkus);
+ } catch (IabException ex) {
+ result = ex.getResult();
+ }
+
+ flagEndAsync();
+
+ final IabResult result_f = result;
+ final Inventory inv_f = inv;
+ if (!mDisposed && listener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ listener.onQueryInventoryFinished(result_f, inv_f);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(true, null, listener);
+ }
+
+ public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(querySkuDetails, null, listener);
+ }
+
+ /**
+ * Consumes a given in-app product. Consuming can only be done on an item
+ * that's owned, and as a result of consumption, the user will no longer own it.
+ * This method may block or take long to return. Do not call from the UI thread.
+ * For that, see {@link #consumeAsync}.
+ *
+ * @param itemInfo The PurchaseInfo that represents the item to consume.
+ * @throws IabException if there is a problem during consumption.
+ */
+ void consume(Purchase itemInfo) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+
+ if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+ throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+ "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+ }
+
+ try {
+ String token = itemInfo.getToken();
+ String sku = itemInfo.getSku();
+ if (token == null || token.equals("")) {
+ logError("Can't consume " + sku + ". No token.");
+ throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ + sku + " " + itemInfo);
+ }
+
+ logDebug("Consuming sku: " + sku + ", token: " + token);
+ int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successfully consumed sku: " + sku);
+ } else {
+ logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+ throw new IabException(response, "Error consuming sku " + sku);
+ }
+ } catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+ }
+ }
+
+ /**
+ * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+ * performs the consumption in the background and notifies completion through
+ * the provided listener. This method is safe to call from a UI thread.
+ *
+ * @param purchase The purchase to be consumed.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ List purchases = new ArrayList<>();
+ purchases.add(purchase);
+ consumeAsyncInternal(purchases, listener, null);
+ }
+
+ /**
+ * Same as {@link consumeAsync}, but for multiple items at once.
+ *
+ * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ consumeAsyncInternal(purchases, null, listener);
+ }
+
+ // Checks that setup was done; if not, throws an exception.
+ void checkSetupDone(String operation) {
+ if (!mSetupDone) {
+ logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+ throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromBundle(Bundle b) {
+ Object o = b.get(RESPONSE_CODE);
+ if (o == null) {
+ logDebug("Bundle with null response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ } else if (o instanceof Integer) return (Integer) o;
+ else if (o instanceof Long) return (int) ((Long) o).longValue();
+ else {
+ logError("Unexpected type for bundle response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromIntent(Intent i) {
+ Object o = i.getExtras().get(RESPONSE_CODE);
+ if (o == null) {
+ logError("Intent with no response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ } else if (o instanceof Integer) return (Integer) o;
+ else if (o instanceof Long) return (int) ((Long) o).longValue();
+ else {
+ logError("Unexpected type for intent response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+ }
+ }
+
+ void flagStartAsync(String operation) {
+ if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
+ operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
+ mAsyncOperation = operation;
+ mAsyncInProgress = true;
+ logDebug("Starting async operation: " + operation);
+ }
+
+ void flagEndAsync() {
+ logDebug("Ending async operation: " + mAsyncOperation);
+ mAsyncOperation = "";
+ mAsyncInProgress = false;
+ }
+
+ int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+ // Query purchases
+ logDebug("Querying owned items, item type: " + itemType);
+ logDebug("Package name: " + mContext.getPackageName());
+ boolean verificationFailed = false;
+ String continueToken = null;
+
+ do {
+ logDebug("Calling getPurchases with continuation token: " + continueToken);
+ Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+ itemType, continueToken);
+
+ int response = getResponseCodeFromBundle(ownedItems);
+ logDebug("Owned items response: " + String.valueOf(response));
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getPurchases() failed: " + getResponseDesc(response));
+ return response;
+ }
+ if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+ logError("Bundle returned from getPurchases() doesn't contain required fields.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+
+ ArrayList ownedSkus = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_ITEM_LIST);
+ ArrayList purchaseDataList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_PURCHASE_DATA_LIST);
+ ArrayList signatureList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_SIGNATURE_LIST);
+
+ for (int i = 0; i < purchaseDataList.size(); ++i) {
+ String purchaseData = purchaseDataList.get(i);
+ String signature = signatureList.get(i);
+ String sku = ownedSkus.get(i);
+ if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+ logDebug("Sku is owned: " + sku);
+ Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+ if (TextUtils.isEmpty(purchase.getToken())) {
+ logWarn("BUG: empty/null token!");
+ logDebug("Purchase data: " + purchaseData);
+ }
+
+ // Record ownership and token
+ inv.addPurchase(purchase);
+ } else {
+ logWarn("Purchase signature verification **FAILED**. Not adding item.");
+ logDebug(" Purchase data: " + purchaseData);
+ logDebug(" Signature: " + signature);
+ verificationFailed = true;
+ }
+ }
+
+ continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+ logDebug("Continuation token: " + continueToken);
+ } while (!TextUtils.isEmpty(continueToken));
+
+ return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+ }
+
+ int querySkuDetails(String itemType, Inventory inv, List moreSkus)
+ throws RemoteException, JSONException {
+ logDebug("Querying SKU details.");
+ ArrayList skuList = new ArrayList<>();
+ skuList.addAll(inv.getAllOwnedSkus(itemType));
+ if (moreSkus != null) {
+ for (String sku : moreSkus) {
+ if (!skuList.contains(sku)) {
+ skuList.add(sku);
+ }
+ }
+ }
+
+ if (skuList.size() == 0) {
+ logDebug("queryPrices: nothing to do because there are no SKUs.");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
+ Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+ itemType, querySkus);
+
+ if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+ int response = getResponseCodeFromBundle(skuDetails);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+ return response;
+ } else {
+ logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+ }
+
+ ArrayList responseList = skuDetails.getStringArrayList(
+ RESPONSE_GET_SKU_DETAILS_LIST);
+
+ for (String thisResponse : responseList) {
+ SkuDetails d = new SkuDetails(itemType, thisResponse);
+ logDebug("Got sku details: " + d);
+ inv.addSkuDetails(d);
+ }
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ void consumeAsyncInternal(final List purchases,
+ final OnConsumeFinishedListener singleListener,
+ final OnConsumeMultiFinishedListener multiListener) {
+ final Handler handler = new Handler();
+ flagStartAsync("consume");
+ (new Thread(new Runnable() {
+ public void run() {
+ final List results = new ArrayList<>();
+ for (Purchase purchase : purchases) {
+ try {
+ consume(purchase);
+ results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+ } catch (IabException ex) {
+ results.add(ex.getResult());
+ }
+ }
+
+ flagEndAsync();
+ if (!mDisposed && singleListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+ }
+ });
+ }
+ if (!mDisposed && multiListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ multiListener.onConsumeMultiFinished(purchases, results);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ void logDebug(String msg) {
+ if (mDebugLog) Log.d(mDebugTag, msg);
+ }
+
+ void logError(String msg) {
+ Log.e(mDebugTag, "In-app billing error: " + msg);
+ }
+
+ void logWarn(String msg) {
+ Log.w(mDebugTag, "In-app billing warning: " + msg);
+ }
+
+ /**
+ * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+ * when the setup process is complete.
+ */
+ public interface OnIabSetupFinishedListener {
+ /**
+ * Called to notify that setup is complete.
+ *
+ * @param result The result of the setup process.
+ */
+ public void onIabSetupFinished(IabResult result);
+ }
+
+
+ /**
+ * Callback that notifies when a purchase is finished.
+ */
+ public interface OnIabPurchaseFinishedListener {
+ /**
+ * Called to notify that an in-app purchase finished. If the purchase was successful,
+ * then the sku parameter specifies which item was purchased. If the purchase failed,
+ * the sku and extraData parameters may or may not be null, depending on how far the purchase
+ * process went.
+ *
+ * @param result The result of the purchase.
+ * @param info The purchase information (null if purchase failed)
+ */
+ public void onIabPurchaseFinished(IabResult result, Purchase info);
+ }
+
+ /**
+ * Listener that notifies when an inventory query operation completes.
+ */
+ public interface QueryInventoryFinishedListener {
+ /**
+ * Called to notify that an inventory query operation completed.
+ *
+ * @param result The result of the operation.
+ * @param inv The inventory.
+ */
+ public void onQueryInventoryFinished(IabResult result, Inventory inv);
+ }
+
+ /**
+ * Callback that notifies when a consumption operation finishes.
+ */
+ public interface OnConsumeFinishedListener {
+ /**
+ * Called to notify that a consumption has finished.
+ *
+ * @param purchase The purchase that was (or was to be) consumed.
+ * @param result The result of the consumption operation.
+ */
+ public void onConsumeFinished(Purchase purchase, IabResult result);
+ }
+
+ /**
+ * Callback that notifies when a multi-item consumption operation finishes.
+ */
+ public interface OnConsumeMultiFinishedListener {
+ /**
+ * Called to notify that a consumption of multiple items has finished.
+ *
+ * @param purchases The purchases that were (or were to be) consumed.
+ * @param results The results of each consumption operation, corresponding to each
+ * sku.
+ */
+ public void onConsumeMultiFinished(List purchases, List results);
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabResult.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabResult.java
new file mode 100644
index 0000000..c01c194
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/IabResult.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+ int mResponse;
+ String mMessage;
+
+ public IabResult(int response, String message) {
+ mResponse = response;
+ if (message == null || message.trim().length() == 0) {
+ mMessage = IabHelper.getResponseDesc(response);
+ } else {
+ mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+ }
+ }
+
+ public int getResponse() {
+ return mResponse;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+
+ public boolean isSuccess() {
+ return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK;
+ }
+
+ public boolean isFailure() {
+ return !isSuccess();
+ }
+
+ public String toString() {
+ return "IabResult: " + getMessage();
+ }
+}
+
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Inventory.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Inventory.java
new file mode 100644
index 0000000..c3b3b23
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Inventory.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+ Map mSkuMap = new HashMap<>();
+ Map mPurchaseMap = new HashMap<>();
+
+ Inventory() {
+ }
+
+ /**
+ * Returns the listing details for an in-app product.
+ */
+ public SkuDetails getSkuDetails(String sku) {
+ return mSkuMap.get(sku);
+ }
+
+ /**
+ * Returns purchase information for a given product, or null if there is no purchase.
+ */
+ public Purchase getPurchase(String sku) {
+ return mPurchaseMap.get(sku);
+ }
+
+ /**
+ * Returns whether or not there exists a purchase of the given product.
+ */
+ public boolean hasPurchase(String sku) {
+ return mPurchaseMap.containsKey(sku);
+ }
+
+ /**
+ * Return whether or not details about the given product are available.
+ */
+ public boolean hasDetails(String sku) {
+ return mSkuMap.containsKey(sku);
+ }
+
+ /**
+ * Erase a purchase (locally) from the inventory, given its product ID. This just
+ * modifies the Inventory object locally and has no effect on the server! This is
+ * useful when you have an existing Inventory object which you know to be up to date,
+ * and you have just consumed an item successfully, which means that erasing its
+ * purchase data from the Inventory you already have is quicker than querying for
+ * a new Inventory.
+ */
+ public void erasePurchase(String sku) {
+ if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+ }
+
+ /**
+ * Returns a list of all owned product IDs.
+ */
+ List getAllOwnedSkus() {
+ return new ArrayList<>(mPurchaseMap.keySet());
+ }
+
+ /**
+ * Returns a list of all owned product IDs of a given type
+ */
+ List getAllOwnedSkus(String itemType) {
+ List result = new ArrayList<>();
+ for (Purchase p : mPurchaseMap.values()) {
+ if (p.getItemType().equals(itemType)) result.add(p.getSku());
+ }
+ return result;
+ }
+
+ /**
+ * Returns a list of all purchases.
+ */
+ List getAllPurchases() {
+ return new ArrayList<>(mPurchaseMap.values());
+ }
+
+ void addSkuDetails(SkuDetails d) {
+ mSkuMap.put(d.getSku(), d);
+ }
+
+ void addPurchase(Purchase p) {
+ mPurchaseMap.put(p.getSku(), p);
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Purchase.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Purchase.java
new file mode 100644
index 0000000..d63575b
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Purchase.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+ String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+ String mOrderId;
+ String mPackageName;
+ String mSku;
+ long mPurchaseTime;
+ int mPurchaseState;
+ String mDeveloperPayload;
+ String mToken;
+ String mOriginalJson;
+ String mSignature;
+
+ public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+ mItemType = itemType;
+ mOriginalJson = jsonPurchaseInfo;
+ JSONObject o = new JSONObject(mOriginalJson);
+ mOrderId = o.optString("orderId");
+ mPackageName = o.optString("packageName");
+ mSku = o.optString("productId");
+ mPurchaseTime = o.optLong("purchaseTime");
+ mPurchaseState = o.optInt("purchaseState");
+ mDeveloperPayload = o.optString("developerPayload");
+ mToken = o.optString("token", o.optString("purchaseToken"));
+ mSignature = signature;
+ }
+
+ public String getItemType() {
+ return mItemType;
+ }
+
+ public String getOrderId() {
+ return mOrderId;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public String getSku() {
+ return mSku;
+ }
+
+ public long getPurchaseTime() {
+ return mPurchaseTime;
+ }
+
+ public int getPurchaseState() {
+ return mPurchaseState;
+ }
+
+ public String getDeveloperPayload() {
+ return mDeveloperPayload;
+ }
+
+ public String getToken() {
+ return mToken;
+ }
+
+ public String getOriginalJson() {
+ return mOriginalJson;
+ }
+
+ public String getSignature() {
+ return mSignature;
+ }
+
+ @Override
+ public String toString() {
+ return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson;
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Security.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Security.java
new file mode 100644
index 0000000..adaed71
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/Security.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the
+ * application on the device. For the sake of simplicity and clarity of this
+ * example, this code is included here and is executed on the device. If you
+ * must verify the purchases on the phone, you should obfuscate this code to
+ * make it harder for an attacker to replace the code with stubs that treat all
+ * purchases as verified.
+ */
+public class Security {
+ private static final String TAG = "IABUtil/Security";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the verified purchase. The data is in JSON format and signed
+ * with a private key. The data also contains the {@link PurchaseState}
+ * and product ID of the purchase.
+ *
+ * @param base64PublicKey the base64-encoded public key to use for verifying.
+ * @param signedData the signed JSON string (signed, not encrypted)
+ * @param signature the signature for the data, signed with the private key
+ */
+ public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+ TextUtils.isEmpty(signature)) {
+ Log.e(TAG, "Purchase verification failed: missing data.");
+ return false;
+ }
+
+ PublicKey key = Security.generatePublicKey(base64PublicKey);
+ return Security.verify(key, signedData, signature);
+ }
+
+ /**
+ * Generates a PublicKey instance from a string containing the
+ * Base64-encoded public key.
+ *
+ * @param encodedPublicKey Base64-encoded public key
+ * @throws IllegalArgumentException if encodedPublicKey is invalid
+ */
+ public static PublicKey generatePublicKey(String encodedPublicKey) {
+ try {
+ byte[] decodedKey = Base64.decode(encodedPublicKey);
+ KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ Log.e(TAG, "Invalid key specification.");
+ throw new IllegalArgumentException(e);
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Verifies that the signature from the server matches the computed
+ * signature on the data. Returns true if the data is correctly signed.
+ *
+ * @param publicKey public key associated with the developer account
+ * @param signedData signed data from server
+ * @param signature server signature
+ * @return true if the data and signature match
+ */
+ public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+ Signature sig;
+ try {
+ sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+ if (!sig.verify(Base64.decode(signature))) {
+ Log.e(TAG, "Signature verification failed.");
+ return false;
+ }
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "NoSuchAlgorithmException.");
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Invalid key specification.");
+ } catch (SignatureException e) {
+ Log.e(TAG, "Signature exception.");
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ }
+ return false;
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/SkuDetails.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/SkuDetails.java
new file mode 100644
index 0000000..acdd42f
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/iab/utils/SkuDetails.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.iab.utils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+ String mItemType;
+ String mSku;
+ String mType;
+ String mPrice;
+ String mTitle;
+ String mDescription;
+ String mJson;
+
+ public SkuDetails(String jsonSkuDetails) throws JSONException {
+ this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+ }
+
+ public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+ mItemType = itemType;
+ mJson = jsonSkuDetails;
+ JSONObject o = new JSONObject(mJson);
+ mSku = o.optString("productId");
+ mType = o.optString("type");
+ mPrice = o.optString("price");
+ mTitle = o.optString("title");
+ mDescription = o.optString("description");
+ }
+
+ public String getSku() {
+ return mSku;
+ }
+
+ public String getType() {
+ return mType;
+ }
+
+ public String getPrice() {
+ return mPrice;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public String toString() {
+ return "SkuDetails:" + mJson;
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/ExtraSettingsActivity.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/ExtraSettingsActivity.java
index e61b918..19aab21 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/ExtraSettingsActivity.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/ExtraSettingsActivity.java
@@ -35,6 +35,7 @@ import java.util.List;
import de.greenrobot.event.EventBus;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.util.EventBusEvents;
+import sharedcode.turboeditor.util.ThemeUtils;
/**
* A {@link PreferenceActivity} that presents a set of application settings. On
@@ -107,12 +108,7 @@ public class ExtraSettingsActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
- boolean light = PreferenceHelper.getLightTheme(this);
- if (light) {
- setTheme(R.style.AppTheme_Light);
- } else {
- setTheme(R.style.AppTheme_Dark);
- }
+ ThemeUtils.setTheme(this);
super.onCreate(savedInstanceState);
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java
index 23f193c..96502b1 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/PreferenceHelper.java
@@ -109,6 +109,9 @@ public final class PreferenceHelper {
return getPrefs(context).getBoolean("page_system_active", true);
}
+ public static boolean hasDonated(Context context) {
+ return getPrefs(context).getBoolean("has_donated", false);
+ }
// Setter methods
public static void setUseMonospace(Context context, boolean value) {
@@ -151,4 +154,8 @@ public final class PreferenceHelper {
getEditor(context).putBoolean("read_only", value).commit();
}
+ public static void setHasDonated(Context context, boolean value) {
+ getEditor(context).putBoolean("has_donated", value);
+ }
+
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java
index d539b4f..cedfde2 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/preferences/SettingsFragment.java
@@ -21,7 +21,6 @@ package sharedcode.turboeditor.preferences;
import android.app.Fragment;
import android.content.Intent;
-import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -32,8 +31,11 @@ import android.widget.TextView;
import de.greenrobot.event.EventBus;
import sharedcode.turboeditor.R;
-import sharedcode.turboeditor.fragment.SeekbarDialogFragment;
+import sharedcode.turboeditor.fragment.SeekbarDialog;
+import sharedcode.turboeditor.util.AnimationUtils;
import sharedcode.turboeditor.util.ProCheckUtils;
+import sharedcode.turboeditor.util.ViewUtils;
+import sharedcode.turboeditor.views.DialogHelper;
import static sharedcode.turboeditor.util.EventBusEvents.APreferenceValueWasChanged;
import static sharedcode.turboeditor.util.EventBusEvents.APreferenceValueWasChanged.Type.FONT_SIZE;
@@ -43,7 +45,7 @@ import static sharedcode.turboeditor.util.EventBusEvents.APreferenceValueWasChan
import static sharedcode.turboeditor.util.EventBusEvents.APreferenceValueWasChanged.Type.SYNTAX;
import static sharedcode.turboeditor.util.EventBusEvents.APreferenceValueWasChanged.Type.WRAP_CONTENT;
-public class SettingsFragment extends Fragment implements SeekbarDialogFragment.onSeekbarDialogDismissed {
+public class SettingsFragment extends Fragment implements SeekbarDialog.ISeekbarDialog {
// Editor Variables
private boolean sLineNumbers;
@@ -67,7 +69,7 @@ public class SettingsFragment extends Fragment implements SeekbarDialogFragment.
Bundle savedInstanceState) {
// Our custom layout
View rootView = inflater.inflate(R.layout.fragment_settings, container, false);
- final CheckBox switchLineNumbers, switchSyntax, switchWrapContent, switchMonospace, switchLightTheme, switchSuggestionsActive, switchAutoSave, switchReadOnly, switchSendErrorReports;
+ final CheckBox switchLineNumbers, switchSyntax, switchWrapContent, switchMonospace, switchReadOnly;
switchLineNumbers = (CheckBox) rootView.findViewById(R.id.switch_line_numbers);
switchSyntax = (CheckBox) rootView.findViewById(R.id.switch_syntax);
switchWrapContent = (CheckBox) rootView.findViewById(R.id.switch_wrap_content);
@@ -80,10 +82,13 @@ public class SettingsFragment extends Fragment implements SeekbarDialogFragment.
switchMonospace.setChecked(sUseMonospace);
switchReadOnly.setChecked(sReadOnly);
- TextView fontSizeView, goProView, extraOptionsView;
+ TextView fontSizeView, donateView, extraOptionsView;
fontSizeView = (TextView) rootView.findViewById(R.id.drawer_button_font_size);
extraOptionsView = (TextView) rootView.findViewById(R.id.drawer_button_extra_options);
- goProView = (TextView) rootView.findViewById(R.id.drawer_button_go_pro);
+ donateView = (TextView) rootView.findViewById(R.id.drawer_button_go_pro);
+
+ if(ProCheckUtils.isPro(getActivity(), false))
+ ViewUtils.setVisible(donateView, false);
switchLineNumbers.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
@@ -139,7 +144,8 @@ public class SettingsFragment extends Fragment implements SeekbarDialogFragment.
int fontCurrent = //(int) (mEditor.getTextSize() / scaledDensity);
//fontMax / 2;
PreferenceHelper.getFontSize(getActivity());
- SeekbarDialogFragment dialogFrag = SeekbarDialogFragment.newInstance(SeekbarDialogFragment.Actions.FileSize, 1, fontCurrent, fontMax);
+ SeekbarDialog dialogFrag = SeekbarDialog.newInstance(SeekbarDialog.Actions
+ .FontSize, 1, fontCurrent, fontMax);
dialogFrag.setTargetFragment(SettingsFragment.this, 0);
dialogFrag.show(getFragmentManager().beginTransaction(), "dialog");
}
@@ -148,27 +154,23 @@ public class SettingsFragment extends Fragment implements SeekbarDialogFragment.
extraOptionsView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- startActivity(new Intent(getActivity(), ExtraSettingsActivity.class));
+ AnimationUtils.startActivityWithScale(getActivity(), new Intent(getActivity(),
+ ExtraSettingsActivity.class), false, 0, v);
}
});
- goProView.setOnClickListener(new View.OnClickListener() {
+ donateView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- try {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.maskyn.fileeditorpro"))
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- } catch (Exception e) {
- }
+ DialogHelper.showDonateDialog(getActivity());
}
});
- goProView.setVisibility(ProCheckUtils.isPro(getActivity()) ? View.GONE : View.VISIBLE);
return rootView;
}
@Override
- public void onSeekbarDialogDismissed(SeekbarDialogFragment.Actions action, int value) {
+ public void onSeekbarDialogDismissed(SeekbarDialog.Actions action, int value) {
PreferenceHelper.setFontSize(getActivity(), value);
EventBus.getDefault().post(new APreferenceValueWasChanged(FONT_SIZE));
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/LinuxShell.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/LinuxShell.java
similarity index 98%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/LinuxShell.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/LinuxShell.java
index 64a59e2..82e2e7c 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/LinuxShell.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/LinuxShell.java
@@ -17,7 +17,9 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.root;
+
+import android.util.Log;
import java.io.BufferedReader;
import java.io.DataOutputStream;
@@ -26,8 +28,6 @@ import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import android.util.Log;
-
public class LinuxShell {
private static final String UNIX_ESCAPE_EXPRESSION = "(\\(|\\)|\\[|\\]|\\s|\'|\"|`|\\{|\\}|&|\\\\|\\?)";
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/RootUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/RootUtils.java
similarity index 98%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/RootUtils.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/RootUtils.java
index b1d7654..7ed8115 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/RootUtils.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/root/RootUtils.java
@@ -32,7 +32,7 @@
* along with 920 Text Editor. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.root;
import android.content.Context;
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/SaveFileTask.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java
similarity index 95%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/SaveFileTask.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java
index 2c94b8e..bc4d5a0 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/SaveFileTask.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/task/SaveFileTask.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.task;
import android.content.Context;
import android.os.AsyncTask;
@@ -32,6 +32,8 @@ import java.util.concurrent.TimeoutException;
import de.greenrobot.event.EventBus;
import sharedcode.turboeditor.R;
+import sharedcode.turboeditor.root.RootUtils;
+import sharedcode.turboeditor.util.EventBusEvents;
public class SaveFileTask extends AsyncTask {
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/EdittextPadding.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java
similarity index 90%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/EdittextPadding.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java
index d4bc9e5..b378e7e 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/EdittextPadding.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/EditTextPadding.java
@@ -17,11 +17,13 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import android.content.Context;
-public class EdittextPadding {
+import sharedcode.turboeditor.util.PixelDipConverter;
+
+public class EditTextPadding {
public static int getPaddingWithoutLineNumbers(Context context) {
return (int) PixelDipConverter.convertDpToPixel(5, context);
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/FileUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java
similarity index 97%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/FileUtils.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java
index f820f67..5ad2a3e 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/FileUtils.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/FileUtils.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import org.mozilla.universalchardet.UniversalDetector;
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/LineUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/LineUtils.java
similarity index 93%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/LineUtils.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/LineUtils.java
index 4472f96..080b42b 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/LineUtils.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/LineUtils.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import android.text.Layout;
import android.widget.ScrollView;
@@ -34,11 +34,11 @@ public class LineUtils {
return realLines;
}
- public int getYAtLine(ScrollView scrollView, int lineCount, int line){
+ public int getYAtLine(ScrollView scrollView, int lineCount, int line) {
return scrollView.getChildAt(0).getHeight() / lineCount * line;
}
- public int getFirstVisibleLine(ScrollView scrollView, int childHeight, int lineCount) throws ArithmeticException{
+ public int getFirstVisibleLine(ScrollView scrollView, int childHeight, int lineCount) throws ArithmeticException {
int line = (scrollView.getScrollY() * lineCount) / childHeight;
if (line < 0) line = 0;
return line;
@@ -85,6 +85,7 @@ public class LineUtils {
/**
* Gets the line from the index of the letter in the text
+ *
* @param index
* @param lineCount
* @param layout
@@ -109,14 +110,14 @@ public class LineUtils {
}
public int lastReadLine() {
- return realLines[realLines.length-1];
+ return realLines[realLines.length - 1];
}
public int fakeLineFromRealLine(int realLine) {
int i;
int fakeLine = 0;
- for(i = 0; i < realLines.length; i++) {
- if (realLine == realLines[i]){
+ for (i = 0; i < realLines.length; i++) {
+ if (realLine == realLines[i]) {
fakeLine = i;
break;
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/PageSystem.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java
similarity index 99%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/PageSystem.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java
index ba90010..d860604 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/PageSystem.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystem.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import android.content.Context;
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/PageSystemButtons.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystemButtons.java
similarity index 99%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/PageSystemButtons.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystemButtons.java
index c7a104d..0f6a50e 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/PageSystemButtons.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/PageSystemButtons.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import android.content.Context;
import android.os.Handler;
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Patterns.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/Patterns.java
similarity index 99%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Patterns.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/Patterns.java
index 06af62f..9c2b630 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Patterns.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/Patterns.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import java.util.regex.Pattern;
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/SearchResult.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/SearchResult.java
similarity index 97%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/SearchResult.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/SearchResult.java
index 634c805..5991dc9 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/SearchResult.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/texteditor/SearchResult.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package sharedcode.turboeditor.util;
+package sharedcode.turboeditor.texteditor;
import java.util.LinkedList;
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java
index 7674655..cadedd9 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AccessStorageApi.java
@@ -19,12 +19,7 @@
package sharedcode.turboeditor.util;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-
import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
@@ -36,174 +31,178 @@ import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
public class AccessStorageApi {
- public static Bitmap loadPrescaledBitmap(String filename) throws IOException {
+ public static Bitmap loadPrescaledBitmap(String filename) throws IOException {
// Facebook image size
- final int IMAGE_MAX_SIZE = 630;
+ final int IMAGE_MAX_SIZE = 630;
- File file = null;
- FileInputStream fis;
+ File file = null;
+ FileInputStream fis;
- BitmapFactory.Options opts;
- int resizeScale;
- Bitmap bmp;
+ BitmapFactory.Options opts;
+ int resizeScale;
+ Bitmap bmp;
- file = new File(filename);
+ file = new File(filename);
- // This bit determines only the width/height of the bitmap without loading the contents
- opts = new BitmapFactory.Options();
- opts.inJustDecodeBounds = true;
- fis = new FileInputStream(file);
- BitmapFactory.decodeStream(fis, null, opts);
- fis.close();
+ // This bit determines only the width/height of the bitmap without loading the contents
+ opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ fis = new FileInputStream(file);
+ BitmapFactory.decodeStream(fis, null, opts);
+ fis.close();
- // Find the correct scale value. It should be a power of 2
- resizeScale = 1;
+ // Find the correct scale value. It should be a power of 2
+ resizeScale = 1;
- if (opts.outHeight > IMAGE_MAX_SIZE || opts.outWidth > IMAGE_MAX_SIZE) {
- resizeScale = (int)Math.pow(2, (int) Math.round(Math.log(IMAGE_MAX_SIZE / (double) Math.max(opts.outHeight, opts.outWidth)) / Math.log(0.5)));
- }
+ if (opts.outHeight > IMAGE_MAX_SIZE || opts.outWidth > IMAGE_MAX_SIZE) {
+ resizeScale = (int) Math.pow(2, (int) Math.round(Math.log(IMAGE_MAX_SIZE / (double) Math.max(opts.outHeight, opts.outWidth)) / Math.log(0.5)));
+ }
- // Load pre-scaled bitmap
- opts = new BitmapFactory.Options();
- opts.inSampleSize = resizeScale;
- fis = new FileInputStream(file);
- bmp = BitmapFactory.decodeStream(fis, null, opts);
+ // Load pre-scaled bitmap
+ opts = new BitmapFactory.Options();
+ opts.inSampleSize = resizeScale;
+ fis = new FileInputStream(file);
+ bmp = BitmapFactory.decodeStream(fis, null, opts);
- fis.close();
+ fis.close();
- return bmp;
- }
-
- /**
- * Get a file path from a Uri. This will get the the path for Storage Access
- * Framework Documents, as well as the _data field for the MediaStore and
- * other file-based ContentProviders.
- *
- * @param context The context.
- * @param uri The Uri to query.
- * @author paulburke
- */
+ return bmp;
+ }
+
+ /**
+ * Get a file path from a Uri. This will get the the path for Storage Access
+ * Framework Documents, as well as the _data field for the MediaStore and
+ * other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @author paulburke
+ */
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
- final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
- // DocumentProvider
+ // DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
- // ExternalStorageProvider
- if (isExternalStorageDocument(uri)) {
- final String docId = DocumentsContract.getDocumentId(uri);
- final String[] split = docId.split(":");
- final String type = split[0];
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
- if ("primary".equalsIgnoreCase(type)) {
- return Environment.getExternalStorageDirectory() + "/" + split[1];
- }
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
- // TODO handle non-primary volumes
- }
- // DownloadsProvider
- else if (isDownloadsDocument(uri)) {
+ // TODO handle non-primary volumes
+ }
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
- final String id = DocumentsContract.getDocumentId(uri);
- final Uri contentUri = ContentUris.withAppendedId(
- Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+ final String id = DocumentsContract.getDocumentId(uri);
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
- return getDataColumn(context, contentUri, null, null);
- }
- // MediaProvider
- else if (isMediaDocument(uri)) {
- final String docId = DocumentsContract.getDocumentId(uri);
- final String[] split = docId.split(":");
- final String type = split[0];
+ return getDataColumn(context, contentUri, null, null);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
- Uri contentUri = null;
- if ("image".equals(type)) {
- contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
- } else if ("video".equals(type)) {
- contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
- } else if ("audio".equals(type)) {
- contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
- }
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
- final String selection = "_id=?";
- final String[] selectionArgs = new String[] {
- split[1]
- };
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[]{
+ split[1]
+ };
- return getDataColumn(context, contentUri, selection, selectionArgs);
- }
- }
- // MediaStore (and general)
- else if ("content".equalsIgnoreCase(uri.getScheme())) {
- return getDataColumn(context, uri, null, null);
- }
- // File
- else if ("file".equalsIgnoreCase(uri.getScheme())) {
- return uri.getPath();
- }
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ }
+ // MediaStore (and general)
+ else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ return getDataColumn(context, uri, null, null);
+ }
+ // File
+ else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
- return null;
- }
+ return null;
+ }
- /**
- * Get the value of the data column for this Uri. This is useful for
- * MediaStore Uris, and other file-based ContentProviders.
- *
- * @param context The context.
- * @param uri The Uri to query.
- * @param selection (Optional) Filter used in the query.
- * @param selectionArgs (Optional) Selection arguments used in the query.
- * @return The value of the _data column, which is typically a file path.
- */
- public static String getDataColumn(Context context, Uri uri, String selection,
- String[] selectionArgs) {
+ /**
+ * Get the value of the data column for this Uri. This is useful for
+ * MediaStore Uris, and other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @param selection (Optional) Filter used in the query.
+ * @param selectionArgs (Optional) Selection arguments used in the query.
+ * @return The value of the _data column, which is typically a file path.
+ */
+ public static String getDataColumn(Context context, Uri uri, String selection,
+ String[] selectionArgs) {
- Cursor cursor = null;
- final String column = "_data";
- final String[] projection = {
- column
- };
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = {
+ column
+ };
- try {
- cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
- null);
- if (cursor != null && cursor.moveToFirst()) {
- final int column_index = cursor.getColumnIndexOrThrow(column);
- return cursor.getString(column_index);
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
- return null;
- }
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ return null;
+ }
- /**
- * @param uri The Uri to check.
- * @return Whether the Uri authority is ExternalStorageProvider.
- */
- public static boolean isExternalStorageDocument(Uri uri) {
- return "com.android.externalstorage.documents".equals(uri.getAuthority());
- }
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ public static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
- /**
- * @param uri The Uri to check.
- * @return Whether the Uri authority is DownloadsProvider.
- */
- public static boolean isDownloadsDocument(Uri uri) {
- return "com.android.providers.downloads.documents".equals(uri.getAuthority());
- }
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ public static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
- /**
- * @param uri The Uri to check.
- * @return Whether the Uri authority is MediaProvider.
- */
- public static boolean isMediaDocument(Uri uri) {
- return "com.android.providers.media.documents".equals(uri.getAuthority());
- }
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AnimationUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AnimationUtils.java
new file mode 100644
index 0000000..f89bb6b
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/AnimationUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+
+package sharedcode.turboeditor.util;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.view.View;
+
+public class AnimationUtils {
+ public static Bundle getScaleBundle(View view) {
+ return ActivityOptionsCompat.makeScaleUpAnimation(
+ view, 0, 0, view.getWidth(), view.getHeight()).toBundle();
+ }
+
+ public static void startActivityWithScale(@NonNull Activity startActivity, @NonNull Intent subActivity, @NonNull boolean forResult, @Nullable int code, @NonNull View view) {
+ if(forResult){
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
+ startActivity.startActivityForResult(subActivity, code, AnimationUtils.getScaleBundle
+ (view));
+ else
+ startActivity.startActivityForResult(subActivity, code);
+ }
+ else {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
+ startActivity.startActivity(subActivity, AnimationUtils.getScaleBundle
+ (view));
+ else
+ startActivity.startActivity(subActivity);
+ }
+ }
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Build.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Build.java
new file mode 100644
index 0000000..c656ccc
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Build.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+package sharedcode.turboeditor.util;
+
+import sharedcode.turboeditor.BuildConfig;
+
+/**
+ * Created by Artem on 30.12.13.
+ */
+public final class Build {
+
+ public static final boolean DEBUG = BuildConfig.DEBUG;
+
+ public static final String SUPPORT_EMAIL = "maskyngames@gmail.com";
+
+ public static final String GOOGLE_PLAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpZca3gZSeRTHPMgxM+A1nTXuRL+9NTOWA1VJFs6ytppcO96i9EWQhmtXVUOKRQIgbXvkepxF7ut+JEjrbniQubZvmyBs9DxK7xUN4Zc3ZboQDQdfg2HJmZXzn8+joOfjXdS9WzsW7aaWKIQ8QXgOB1RUm9hdMdAlgKw+cEyp27WOoMK5m2H/i7C0MIO9tEQs3Hn9UTjayzzfy3MY+KDaX3T6oKievegbpyqyt8y4cpVusJC+uQFLa4bHKPtA3MaPUG6kU9tRV/DHrvFV6dOaPuTYCnYJELlGNfeqRUF0Nvb3Sv0U+BUoXgevjrlLdLz1bqgPDibLzaQmmofNXOnVQIDAQAB";
+
+ public static final int MAX_FILE_SIZE = 20_000;
+
+ public static final boolean FOR_AMAZON = false;
+
+ public static class Links {
+
+ public static final String GITHUB = "http://github.com/vmihalachi/TurboEditor";
+
+ public static final String XDA = "http://forum.xda-developers.com/android/apps-games/app-turbo-editor-text-editor-t2832016";
+
+ public static final String TRANSLATE = "http://crowdin.net/project/turbo-client";
+
+ public static final String DONATE = "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=26VWS2TSAMUJA";
+
+ public static final String GOOGLE_PLUS_COMMUNITY = "http://plus.google.com/u/0/communities/111974095419108178946";
+
+ public static final String AMAZON_STORE = "amzn://apps/android?p=com.maskyn.fileeditor";
+
+ public static final String PLAY_STORE = "market://search?q=pub:Maskyn";
+
+ }
+
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Constants.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Constants.java
deleted file mode 100644
index d8c5959..0000000
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Constants.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2014 Vlad Mihalachi
- *
- * This file is part of Turbo Editor.
- *
- * Turbo Editor 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.
- *
- * Turbo Editor 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 .
- */
-
-package sharedcode.turboeditor.util;
-
-public class Constants {
- public static final int MAX_FILE_SIZE = 20_000;
- public static final boolean FOR_AMAZON = false;
-}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java
new file mode 100644
index 0000000..ca464f6
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/Device.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+package sharedcode.turboeditor.util;
+
+import android.os.Build;
+
+/**
+ * Contains params of current device. This is nice because we can override
+ * some here to test compatibility with old API.
+ *
+ * @author Artem Chepurnoy
+ */
+public class Device {
+
+ /**
+ * @return {@code true} if device is device supports given API version,
+ * {@code false} otherwise.
+ */
+ public static boolean hasTargetApi(int api) {
+ return Build.VERSION.SDK_INT >= api;
+ }
+
+ /**
+ * @return {@code true} if device is running
+ * {@link android.os.Build.VERSION_CODES#L Lemon Cake} or higher, {@code false} otherwise.
+ */
+ public static boolean hasLemonCakeApi() {
+ return Build.VERSION.SDK_INT >= 20; // Build.VERSION_CODES.L;
+ }
+
+ /**
+ * @return {@code true} if device is running
+ * {@link android.os.Build.VERSION_CODES#KITKAT KitKat} or higher, {@code false} otherwise.
+ */
+ public static boolean hasKitKatApi() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ }
+
+ /**
+ * @return {@code true} if device is running
+ * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2 Jelly Bean 4.3} or higher, {@code false} otherwise.
+ */
+ public static boolean hasJellyBeanMR2Api() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ }
+
+ /**
+ * @return {@code true} if device is running
+ * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1 Jelly Bean 4.2} or higher, {@code false} otherwise.
+ */
+ public static boolean hasJellyBeanMR1Api() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
+ }
+
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/EditorInterface.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/EditorInterface.java
deleted file mode 100644
index 491f042..0000000
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/EditorInterface.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2014 Vlad Mihalachi
- *
- * This file is part of Turbo Editor.
- *
- * Turbo Editor 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.
- *
- * Turbo Editor 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 .
- */
-
-package sharedcode.turboeditor.util;
-
-import sharedcode.turboeditor.views.GoodScrollView;
-
-public interface EditorInterface {
- public GoodScrollView getVerticalScrollView();
-
- public String getFilePath();
-
- public PageSystem getPageSystem();
-
- public void updateTextSyntax();
-}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ProCheckUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ProCheckUtils.java
index 40b9040..f909309 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ProCheckUtils.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ProCheckUtils.java
@@ -21,16 +21,24 @@ package sharedcode.turboeditor.util;
import android.content.Context;
+import sharedcode.turboeditor.preferences.PreferenceHelper;
+
public class ProCheckUtils {
- public static boolean isPro(Context context) {
+ public static boolean isPro(Context context, boolean includeDonations) {
String packageName = context.getPackageName();
- if (Constants.FOR_AMAZON)
+ if (Build.FOR_AMAZON)
return true;
else if (packageName.equals("com.maskyn.fileeditorpro"))
return true;
+ else if (includeDonations && PreferenceHelper.hasDonated(context))
+ return true;
else
return false;
}
+
+ public static boolean isPro(Context context) {
+ return isPro(context, true);
+ }
}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ThemeHelper.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ThemeUtils.java
similarity index 80%
rename from libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ThemeHelper.java
rename to libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ThemeUtils.java
index 7bbf6ce..ac76d63 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ThemeHelper.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ThemeUtils.java
@@ -24,7 +24,16 @@ import android.app.Activity;
import sharedcode.turboeditor.R;
import sharedcode.turboeditor.preferences.PreferenceHelper;
-public class ThemeHelper {
+public class ThemeUtils {
+
+ public static void setTheme(Activity activity){
+ boolean light = PreferenceHelper.getLightTheme(activity);
+ if (light) {
+ activity.setTheme(R.style.AppThemeBaseLight);
+ } else {
+ activity.setTheme(R.style.AppThemeBaseDark);
+ }
+ }
public static void setWindowsBackground(Activity activity) {
boolean whiteTheme = PreferenceHelper.getLightTheme(activity);
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ToastUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ToastUtils.java
new file mode 100644
index 0000000..ff6a5bb
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ToastUtils.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+package sharedcode.turboeditor.util;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.widget.Toast;
+
+/**
+ * Helper class with utils related to toasts (no bacon.)
+ *
+ * @author Artem Chepurnoy
+ */
+public class ToastUtils {
+
+ /**
+ * Shows toast message with given message shortly.
+ *
+ * @param text message to show
+ * @see #showLong(android.content.Context, CharSequence)
+ */
+ @NonNull
+ public static Toast showShort(@NonNull Context context, @NonNull CharSequence text) {
+ return show(context, text, Toast.LENGTH_SHORT);
+ }
+
+ @NonNull
+ public static Toast showShort(@NonNull Context context, int stringRes) {
+ return showShort(context, context.getString(stringRes));
+ }
+
+ /**
+ * Shows toast message with given message for a long time.
+ *
+ * @param text message to show
+ * @see #showShort(android.content.Context, CharSequence)
+ */
+ @NonNull
+ public static Toast showLong(@NonNull Context context, @NonNull CharSequence text) {
+ return show(context, text, Toast.LENGTH_LONG);
+ }
+
+ @NonNull
+ public static Toast showLong(@NonNull Context context, int stringRes) {
+ return showLong(context, context.getString(stringRes));
+ }
+
+ @NonNull
+ private static Toast show(@NonNull Context context, CharSequence text, int duration) {
+ Toast toast = Toast.makeText(context, text, duration);
+ toast.show();
+ return toast;
+ }
+
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ViewUtils.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ViewUtils.java
new file mode 100644
index 0000000..8459586
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/util/ViewUtils.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+package sharedcode.turboeditor.util;
+
+import android.graphics.Matrix;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.TextView;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Created by Artem on 21.01.14.
+ */
+public class ViewUtils {
+
+ private static final String TAG = "ViewUtils";
+
+ @NonNull
+ private static final MotionEventHandler MOTION_EVENT_HANDLER = Device.hasKitKatApi()
+ ? new MotionEventHandlerReflection()
+ : new MotionEventHandlerCompat();
+
+ public static boolean isTouchPointInView(@NonNull View view, float x, float y) {
+ final int[] coordinates = new int[3];
+ view.getLocationInWindow(coordinates);
+ int left = coordinates[0];
+ int top = coordinates[1];
+ return x >= left && x <= left + view.getWidth() &&
+ y >= top && y <= top + view.getHeight();
+ }
+
+ public static int getLeft(@NonNull View view) {
+ final int[] coordinates = new int[3];
+ view.getLocationInWindow(coordinates);
+ return coordinates[0];
+ }
+
+ public static int getTop(@NonNull View view) {
+ final int[] coordinates = new int[3];
+ view.getLocationInWindow(coordinates);
+ return coordinates[1];
+ }
+
+ public static int getBottom(@NonNull View view) {
+ return getTop(view) + view.getHeight();
+ }
+
+ public static int indexOf(@NonNull ViewGroup container, @NonNull View view) {
+ int length = container.getChildCount();
+ for (int i = 0; i < length; i++) {
+ View child = container.getChildAt(i);
+ assert child != null;
+
+ if (child.equals(view)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // //////////////////////////////////////////
+ // //////////// -- VISIBILITY -- ////////////
+ // //////////////////////////////////////////
+
+ public static void setVisible(@NonNull View view, boolean visible) {
+ setVisible(view, visible, View.GONE);
+ }
+
+ public static void setVisible(@NonNull View view, boolean visible, int invisibleFlag) {
+ int visibility = view.getVisibility();
+ int visibilityNew = visible ? View.VISIBLE : invisibleFlag;
+
+ if (visibility != visibilityNew) {
+ view.setVisibility(visibilityNew);
+ }
+ }
+
+ public static void safelySetText(@NonNull TextView textView, @Nullable CharSequence text) {
+ final boolean visible = text != null;
+ if (visible) textView.setText(text);
+ ViewUtils.setVisible(textView, visible);
+ }
+
+ // //////////////////////////////////////////
+ // /////////// -- TOUCH EVENTS -- ///////////
+ // //////////////////////////////////////////
+
+ public static boolean pointInView(@NonNull View view, float localX, float localY, float slop) {
+ return localX >= view.getLeft() - slop
+ && localX < view.getRight() + slop
+ && localY >= view.getTop() - slop
+ && localY < view.getBottom() + slop;
+ }
+
+ /**
+ * Transforms a motion event from view-local coordinates to on-screen
+ * coordinates.
+ *
+ * @param ev the view-local motion event
+ * @return false if the transformation could not be applied
+ */
+ public static boolean toGlobalMotionEvent(@NonNull View view, @NonNull MotionEvent ev) {
+ return MOTION_EVENT_HANDLER.toGlobalMotionEvent(view, ev);
+ }
+
+ /**
+ * Transforms a motion event from on-screen coordinates to view-local
+ * coordinates.
+ *
+ * @param ev the on-screen motion event
+ * @return false if the transformation could not be applied
+ */
+ public static boolean toLocalMotionEvent(@NonNull View view, @NonNull MotionEvent ev) {
+ return MOTION_EVENT_HANDLER.toLocalMotionEvent(view, ev);
+ }
+
+ private static abstract class MotionEventHandler {
+
+ /**
+ * Transforms a motion event from view-local coordinates to on-screen
+ * coordinates.
+ *
+ * @param ev the view-local motion event
+ * @return false if the transformation could not be applied
+ */
+ abstract boolean toGlobalMotionEvent(@NonNull View view, @NonNull MotionEvent ev);
+
+ /**
+ * Transforms a motion event from on-screen coordinates to view-local
+ * coordinates.
+ *
+ * @param ev the on-screen motion event
+ * @return false if the transformation could not be applied
+ */
+ abstract boolean toLocalMotionEvent(@NonNull View view, @NonNull MotionEvent ev);
+
+ }
+
+ private static final class MotionEventHandlerReflection extends MotionEventHandler {
+
+ @Override
+ boolean toGlobalMotionEvent(@NonNull View view, @NonNull MotionEvent ev) {
+ return toMotionEvent(view, ev, "toGlobalMotionEvent");
+ }
+
+ @Override
+ boolean toLocalMotionEvent(@NonNull View view, @NonNull MotionEvent ev) {
+ return toMotionEvent(view, ev, "toLocalMotionEvent");
+ }
+
+ private boolean toMotionEvent(View view, MotionEvent ev, String methodName) {
+ try {
+ Method method = View.class.getDeclaredMethod(methodName, MotionEvent.class);
+ method.setAccessible(true);
+ return (boolean) method.invoke(view, ev);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | NoClassDefFoundError e) {
+ Log.wtf(TAG, "Failed to access motion event transforming!!!");
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ }
+
+ private static final class MotionEventHandlerCompat extends MotionEventHandler {
+
+ @Nullable
+ private static int[] getWindowPosition(@NonNull View view) {
+ Object info;
+ try {
+ Field field = View.class.getDeclaredField("mAttachInfo");
+ field.setAccessible(true);
+ info = field.get(view);
+ } catch (Exception e) {
+ info = null;
+ Log.e(TAG, "Failed to get AttachInfo.");
+ }
+
+ if (info == null) {
+ return null;
+ }
+
+ int[] position = new int[2];
+
+ try {
+ Class clazz = Class.forName("android.view.View$AttachInfo");
+
+ Field field = clazz.getDeclaredField("mWindowLeft");
+ field.setAccessible(true);
+ position[0] = field.getInt(info);
+
+ field = clazz.getDeclaredField("mWindowTop");
+ field.setAccessible(true);
+ position[1] = field.getInt(info);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get window\'s position from AttachInfo.");
+ return null;
+ }
+
+ return position;
+ }
+
+ /**
+ * Recursive helper method that applies transformations in post-order.
+ *
+ * @param ev the on-screen motion event
+ */
+ private static void transformMotionEventToLocal(@NonNull View view, @NonNull MotionEvent ev) {
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ transformMotionEventToLocal(vp, ev);
+ ev.offsetLocation(vp.getScrollX(), vp.getScrollY());
+ } // TODO: Use reflections to access ViewRootImpl
+ // else if (parent instanceof ViewRootImpl) {
+ // final ViewRootImpl vr = (ViewRootImpl) parent;
+ // ev.offsetLocation(0, vr.mCurScrollY);
+ // }
+
+ ev.offsetLocation(-view.getLeft(), -view.getTop());
+
+ Matrix matrix = view.getMatrix();
+ if (matrix != null) {
+ ev.transform(matrix);
+ }
+ }
+
+ /**
+ * Recursive helper method that applies transformations in pre-order.
+ *
+ * @param ev the on-screen motion event
+ */
+ private static void transformMotionEventToGlobal(@NonNull View view, @NonNull MotionEvent ev) {
+ Matrix matrix = view.getMatrix();
+ if (matrix != null) {
+ ev.transform(matrix);
+ }
+
+ ev.offsetLocation(view.getLeft(), view.getTop());
+
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ ev.offsetLocation(-vp.getScrollX(), -vp.getScrollY());
+ transformMotionEventToGlobal(vp, ev);
+ } // TODO: Use reflections to access ViewRootImpl
+ // else if (parent instanceof ViewRootImpl) {
+ // final ViewRootImpl vr = (ViewRootImpl) parent;
+ // ev.offsetLocation(0, -vr.mCurScrollY);
+ // }
+ }
+
+ @Override
+ boolean toGlobalMotionEvent(@NonNull View view, @NonNull MotionEvent ev) {
+ final int[] windowPosition = getWindowPosition(view);
+ if (windowPosition == null) {
+ return false;
+ }
+
+ transformMotionEventToGlobal(view, ev);
+ ev.offsetLocation(windowPosition[0], windowPosition[1]);
+ return true;
+ }
+
+ @Override
+ boolean toLocalMotionEvent(@NonNull View view, @NonNull MotionEvent ev) {
+ final int[] windowPosition = getWindowPosition(view);
+ if (windowPosition == null) {
+ return false;
+ }
+
+ ev.offsetLocation(-windowPosition[0], -windowPosition[1]);
+ transformMotionEventToLocal(view, ev);
+ return true;
+ }
+ }
+
+}
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/DialogHelper.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/DialogHelper.java
new file mode 100644
index 0000000..5e1dac3
--- /dev/null
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/DialogHelper.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2014 Vlad Mihalachi
+ *
+ * This file is part of Turbo Editor.
+ *
+ * Turbo Editor 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.
+ *
+ * Turbo Editor 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 .
+ */
+package sharedcode.turboeditor.views;
+
+/**
+ * Created by Artem on 28.01.14.
+ */
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import sharedcode.turboeditor.R;
+import sharedcode.turboeditor.fragment.AboutDialog;
+import sharedcode.turboeditor.iab.DonationFragment;
+
+/**
+ * Helper class for showing fragment dialogs.
+ */
+public class DialogHelper {
+
+ public static final String TAG_FRAGMENT_ABOUT = "dialog_about";
+ public static final String TAG_FRAGMENT_HELP = "dialog_help";
+ public static final String TAG_FRAGMENT_DONATION = "dialog_donate";
+ public static final String TAG_FRAGMENT_FEEDBACK = "dialog_feedback";
+
+ public static void showDonateDialog(Activity activity) {
+ showDialog(activity, DonationFragment.class, TAG_FRAGMENT_DONATION);
+ }
+
+ public static void showAboutDialog(Activity activity) {
+ showDialog(activity, AboutDialog.class, TAG_FRAGMENT_ABOUT);
+ }
+
+ private static void showDialog(Activity activity, Class clazz, String tag) {
+ FragmentManager fm = activity.getFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+ Fragment prev = fm.findFragmentByTag(tag);
+ if (prev != null) {
+ ft.remove(prev);
+ }
+ ft.addToBackStack(null);
+
+ try {
+ ((DialogFragment) clazz.newInstance()).show(ft, tag);
+ } catch (InstantiationException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Helper class to implement custom dialog's design.
+ */
+ public static class Builder {
+
+ /**
+ * Layout where content's layout exists in a {@link android.widget.ScrollView}.
+ * This is nice to display simple layout without scrollable elements such as
+ * {@link android.widget.ListView} or any similar. Use {@link #LAYOUT_SKELETON}
+ * for them.
+ *
+ * @see #LAYOUT_SKELETON
+ * @see #createCommonView()
+ * @see #wrap(int)
+ */
+ public static final int LAYOUT_COMMON = 0;
+
+ /**
+ * The skeleton of dialog's layout. The only thing that is here is the custom
+ * view you set and the title / icon. Use it to display scrollable elements such as
+ * {@link android.widget.ListView}.
+ *
+ * @see #LAYOUT_COMMON
+ * @see #createSkeletonView()
+ * @see #wrap(int)
+ */
+ public static final int LAYOUT_SKELETON = 1;
+
+ protected final Context mContext;
+
+ private Drawable mIcon;
+ private CharSequence mTitleText;
+ private CharSequence mMessageText;
+ private View mView;
+ private int mViewRes;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(201, 17)
+ .append(mContext)
+ .append(mIcon)
+ .append(mTitleText)
+ .append(mMessageText)
+ .append(mViewRes)
+ .append(mView)
+ .toHashCode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o == null)
+ return false;
+ if (o == this)
+ return true;
+ if (!(o instanceof Builder))
+ return false;
+
+ Builder builder = (Builder) o;
+ return new EqualsBuilder()
+ .append(mContext, builder.mContext)
+ .append(mIcon, builder.mIcon)
+ .append(mTitleText, builder.mTitleText)
+ .append(mMessageText, builder.mMessageText)
+ .append(mViewRes, builder.mViewRes)
+ .append(mView, builder.mView)
+ .isEquals();
+ }
+
+ public Builder setIcon(Drawable icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ public Builder setTitle(CharSequence title) {
+ mTitleText = title;
+ return this;
+ }
+
+ public Builder setMessage(CharSequence message) {
+ mMessageText = message;
+ return this;
+ }
+
+ public Builder setIcon(int iconRes) {
+ return setIcon(iconRes == 0 ? null : mContext.getResources().getDrawable(iconRes));
+ }
+
+ public Builder setTitle(int titleRes) {
+ return setTitle(titleRes == 0 ? null : getString(titleRes));
+ }
+
+ public Builder setMessage(int messageRes) {
+ return setMessage(messageRes == 0 ? null : getString(messageRes));
+ }
+
+ private String getString(int stringRes) {
+ return mContext.getResources().getString(stringRes);
+ }
+
+ public Builder setView(View view) {
+ mView = view;
+ mViewRes = 0;
+ return this;
+ }
+
+ public Builder setView(int layoutRes) {
+ mView = null;
+ mViewRes = layoutRes;
+ return this;
+ }
+
+ /**
+ * Builds dialog's view
+ *
+ * @throws IllegalArgumentException when type is not one of defined.
+ * @see #LAYOUT_COMMON
+ * @see #LAYOUT_SKELETON
+ */
+ public View createView(int type) {
+ switch (type) {
+ case LAYOUT_COMMON:
+ return createCommonView();
+ case LAYOUT_SKELETON:
+ return createSkeletonView();
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Builds view that based on simple {@link android.widget.ScrollView} container.
+ * This is nice to display simple layout without scrollable elements such as
+ * {@link android.widget.ListView} or any similar. Use {@link #createSkeletonView()}
+ * for them.
+ *
+ * @see #LAYOUT_COMMON
+ * @see #createView(int)
+ */
+ public View createCommonView() {
+
+ // Creating skeleton layout will also try
+ // to add custom view. Avoid of doing it.
+ int customViewRes = mViewRes;
+ View customView = mView;
+ mViewRes = 0;
+ mView = null;
+
+ LayoutInflater inflater = (LayoutInflater) mContext
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ ViewGroup rootLayout = (ViewGroup) createSkeletonView();
+ View bodyRootView = inflater.inflate(R.layout.dialog, rootLayout, false);
+ ViewGroup bodyLayout = (ViewGroup) bodyRootView.findViewById(R.id.content);
+ TextView messageView = (TextView) bodyLayout.findViewById(R.id.message);
+
+ rootLayout.addView(bodyRootView);
+
+ // Setup content
+ bodyLayout.removeView(messageView);
+ if (!TextUtils.isEmpty(mMessageText)) {
+ messageView.setMovementMethod(new LinkMovementMethod());
+ messageView.setText(mMessageText);
+ bodyLayout.addView(messageView);
+ }
+
+ // Custom view
+ if (customViewRes != 0) customView = inflater.inflate(customViewRes, bodyLayout, false);
+ if (customView != null) bodyLayout.addView(customView);
+ mView = customView;
+
+ return rootLayout;
+ }
+
+ /**
+ * @see #LAYOUT_SKELETON
+ * @see #createView(int)
+ */
+ public View createSkeletonView() {
+ LayoutInflater inflater = (LayoutInflater) mContext
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ ViewGroup rootLayout = (ViewGroup) inflater.inflate(R.layout.dialog_skeleton, null);
+ TextView titleView = (TextView) rootLayout.findViewById(R.id.title);
+
+ // Setup title
+ if (mTitleText != null) {
+ titleView.setText(mTitleText);
+ titleView.setCompoundDrawablesWithIntrinsicBounds(mIcon, null, null, null);
+ } else {
+ // This also removes an icon.
+ rootLayout.removeView(titleView);
+ }
+
+ // Custom view
+ if (mViewRes != 0) mView = inflater.inflate(mViewRes, rootLayout, false);
+ if (mView != null) rootLayout.addView(mView);
+
+ return rootLayout;
+ }
+
+ public AlertDialog.Builder wrap() {
+ return wrap(LAYOUT_COMMON);
+ }
+
+ /**
+ * Creates view and {@link android.app.AlertDialog.Builder#setView(android.view.View) sets}
+ * to new {@link android.app.AlertDialog.Builder}.
+ *
+ * @param type type of container layout
+ * @return AlertDialog.Builder with set custom view
+ */
+ public AlertDialog.Builder wrap(int type) {
+ return new AlertDialog.Builder(mContext).setView(createView(type));
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/GoodScrollView.java b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/GoodScrollView.java
index 459fbba..a1d9942 100644
--- a/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/GoodScrollView.java
+++ b/libraries/sharedCode/src/main/java/sharedcode/turboeditor/views/GoodScrollView.java
@@ -22,7 +22,6 @@ package sharedcode.turboeditor.views;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
-import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
@@ -61,10 +60,10 @@ public class GoodScrollView extends ScrollView {
}
- public boolean hasReachedBottom(){
- View firstChild = getChildAt(getChildCount()-1);
+ public boolean hasReachedBottom() {
+ View firstChild = getChildAt(getChildCount() - 1);
- int diff = (firstChild.getBottom()-(getHeight()+getScrollY()+firstChild.getTop()));// Calculate the scrolldiff
+ int diff = (firstChild.getBottom() - (getHeight() + getScrollY() + firstChild.getTop()));// Calculate the scrolldiff
return diff <= 0;
}
diff --git a/libraries/sharedCode/src/main/res/drawable-hdpi/ic_action_paypal.png b/libraries/sharedCode/src/main/res/drawable-hdpi/ic_action_paypal.png
new file mode 100644
index 0000000..d11ddbc
Binary files /dev/null and b/libraries/sharedCode/src/main/res/drawable-hdpi/ic_action_paypal.png differ
diff --git a/libraries/sharedCode/src/main/res/drawable-mdpi/ic_action_paypal.png b/libraries/sharedCode/src/main/res/drawable-mdpi/ic_action_paypal.png
new file mode 100644
index 0000000..7844e6d
Binary files /dev/null and b/libraries/sharedCode/src/main/res/drawable-mdpi/ic_action_paypal.png differ
diff --git a/libraries/sharedCode/src/main/res/drawable-xhdpi/ic_action_paypal.png b/libraries/sharedCode/src/main/res/drawable-xhdpi/ic_action_paypal.png
new file mode 100644
index 0000000..7feddfa
Binary files /dev/null and b/libraries/sharedCode/src/main/res/drawable-xhdpi/ic_action_paypal.png differ
diff --git a/libraries/sharedCode/src/main/res/drawable-xxhdpi/ic_action_paypal.png b/libraries/sharedCode/src/main/res/drawable-xxhdpi/ic_action_paypal.png
new file mode 100644
index 0000000..a1638ae
Binary files /dev/null and b/libraries/sharedCode/src/main/res/drawable-xxhdpi/ic_action_paypal.png differ
diff --git a/libraries/sharedCode/src/main/res/drawable/item_background_holo_dark.xml b/libraries/sharedCode/src/main/res/drawable/item_background_holo_dark.xml
index c9797ae..b548cc6 100644
--- a/libraries/sharedCode/src/main/res/drawable/item_background_holo_dark.xml
+++ b/libraries/sharedCode/src/main/res/drawable/item_background_holo_dark.xml
@@ -26,5 +26,5 @@
-
+
diff --git a/libraries/sharedCode/src/main/res/layout/activity_about.xml b/libraries/sharedCode/src/main/res/layout/activity_about.xml
deleted file mode 100644
index 2a45a32..0000000
--- a/libraries/sharedCode/src/main/res/layout/activity_about.xml
+++ /dev/null
@@ -1,149 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/layout/activity_home.xml b/libraries/sharedCode/src/main/res/layout/activity_home.xml
index 8b83f1e..e24b75a 100644
--- a/libraries/sharedCode/src/main/res/layout/activity_home.xml
+++ b/libraries/sharedCode/src/main/res/layout/activity_home.xml
@@ -25,14 +25,81 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:textSize="@dimen/text_size_mega_title"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/open_a_file"
+ android:background="@null"
+ android:textColor="@android:color/secondary_text_dark"
+ android:id="@id/no_file_opened_messagge"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:smoothScrollbar="true"/>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/layout/dialog_fragment_find_text.xml b/libraries/sharedCode/src/main/res/layout/dialog_fragment_find_text.xml
index ddd7abe..d7cf699 100644
--- a/libraries/sharedCode/src/main/res/layout/dialog_fragment_find_text.xml
+++ b/libraries/sharedCode/src/main/res/layout/dialog_fragment_find_text.xml
@@ -19,7 +19,7 @@
-->
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/layout/donation_dialog.xml b/libraries/sharedCode/src/main/res/layout/donation_dialog.xml
new file mode 100644
index 0000000..2132d23
--- /dev/null
+++ b/libraries/sharedCode/src/main/res/layout/donation_dialog.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/layout/donation_iab_item.xml b/libraries/sharedCode/src/main/res/layout/donation_iab_item.xml
new file mode 100644
index 0000000..f74296b
--- /dev/null
+++ b/libraries/sharedCode/src/main/res/layout/donation_iab_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/layout/fragment_editor.xml b/libraries/sharedCode/src/main/res/layout/fragment_editor.xml
deleted file mode 100644
index 6b163ec..0000000
--- a/libraries/sharedCode/src/main/res/layout/fragment_editor.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/layout/fragment_settings.xml b/libraries/sharedCode/src/main/res/layout/fragment_settings.xml
index 5af7835..1102951 100644
--- a/libraries/sharedCode/src/main/res/layout/fragment_settings.xml
+++ b/libraries/sharedCode/src/main/res/layout/fragment_settings.xml
@@ -31,16 +31,11 @@
android:orientation="vertical"
>
-
-
+
+ Turbo Editor is a free and open source app. Now you can show your appreciation and support development by donating :)
+ New visual changes
+ New save dialog when you are about to close a file
+ Many enchantments and fixes
+
+
Many enchantments and fixes
diff --git a/libraries/sharedCode/src/main/res/values-sw600dp/dimens.xml b/libraries/sharedCode/src/main/res/values-w820dp/dimens.xml
similarity index 96%
rename from libraries/sharedCode/src/main/res/values-sw600dp/dimens.xml
rename to libraries/sharedCode/src/main/res/values-w820dp/dimens.xml
index b43a9db..d1b4929 100644
--- a/libraries/sharedCode/src/main/res/values-sw600dp/dimens.xml
+++ b/libraries/sharedCode/src/main/res/values-w820dp/dimens.xml
@@ -20,7 +20,6 @@
64dp
- 64dp
diff --git a/libraries/sharedCode/src/main/res/layout/fragment_no_file_open.xml b/libraries/sharedCode/src/main/res/values/attrs.xml
similarity index 66%
rename from libraries/sharedCode/src/main/res/layout/fragment_no_file_open.xml
rename to libraries/sharedCode/src/main/res/values/attrs.xml
index 35ea9fa..573577d 100644
--- a/libraries/sharedCode/src/main/res/layout/fragment_no_file_open.xml
+++ b/libraries/sharedCode/src/main/res/values/attrs.xml
@@ -18,12 +18,12 @@
~ along with this program. If not, see .
-->
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/values/colors.xml b/libraries/sharedCode/src/main/res/values/colors.xml
index 9e92015..e54a7c7 100644
--- a/libraries/sharedCode/src/main/res/values/colors.xml
+++ b/libraries/sharedCode/src/main/res/values/colors.xml
@@ -61,4 +61,7 @@
#40969696
#50000000
#30000000
+
+ #b6cc45
+ #DFED47
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/values/dimens.xml b/libraries/sharedCode/src/main/res/values/dimens.xml
index 69dca58..09562e1 100644
--- a/libraries/sharedCode/src/main/res/values/dimens.xml
+++ b/libraries/sharedCode/src/main/res/values/dimens.xml
@@ -19,8 +19,8 @@
- 0dp
- 0dp
+ 16dp
+ 16dp
diff --git a/libraries/sharedCode/src/main/res/values/ids.xml b/libraries/sharedCode/src/main/res/values/ids.xml
index 14c2aae..008c7a1 100644
--- a/libraries/sharedCode/src/main/res/values/ids.xml
+++ b/libraries/sharedCode/src/main/res/values/ids.xml
@@ -50,7 +50,6 @@
-
@@ -59,6 +58,9 @@
+
+
+
diff --git a/libraries/sharedCode/src/main/res/values/strings.xml b/libraries/sharedCode/src/main/res/values/strings.xml
index b281ae6..0cb3be0 100644
--- a/libraries/sharedCode/src/main/res/values/strings.xml
+++ b/libraries/sharedCode/src/main/res/values/strings.xml
@@ -30,7 +30,8 @@
File
Folder
Light Theme
- Go to Line
+ Go to Line…
+ Go to Page…
Find
Replace
Share
@@ -46,12 +47,11 @@
Previous
Please wait…
%s occurrences was found
- Version %s
+ v%s
Translate
Changelog
Match case
Long click for more options
- Pro version
Auto save
Read only
Send error reports
@@ -59,116 +59,18 @@
Split the text if too long
Ignore back button
Donate
-
- New account
- Active
- Delete
- Deleting files…
- Loading…
- Current local folder
- Private Key
- Light
Encoding
Share
- New local folder
- New remote folder
- New remote file
- New local file
- Disconnect
- Default local folder
- Where to download?
- Download
- Download completed
- Duplicate
- Done
- Home
- Host
Info
- Local
- Logging in…
- Edit
- Move
- Hide
- Turbo Client
Turbo Editor
- Username
- Passive
- Passphrase
- Password
- Leave it empty to prompt for it every session
- Port
Preferences
- Remote
- To change the theme, restart the application
- Rename
- Default remote folder
Save
- Dark
- Select
- Select an account
- Are you sure?
- Something failed
- Do not transfer same file
- App theme
- Connection type
- Protocol type
- Another folder
- Use a passphrase
- Upload
- Upload completed
- What do you want to do?
Syntax highlight
Undo
Redo
- Sync
- Remote folder to sync
- Local folder to sync
- Rate
- Cannot contact Google Play
- Support the development of other great features.
- Upgrade to Premium
- Upgrade to Premium and support the development of Turbo Client!
- Download unlocked version
- What is Turbo Client worth to you? Set your price!
- Upgrade to unlock this features:
- Power to open and modify any type of file.
- Backup service to backup and restore your data safely.
- Unlock the Premium features
- I really like this app!
- I love this app!
- Backup the accounts
- Restore the accounts
- Backup and share the accounts
- Importing the accounts…
- Exporting the accounts...
- No backups found
- Cannot open the file
- Temporary folder does not exist
- An error occurred
- UI
- Remove
- Modification date
- Name
- Size
- Sort
Open
- The file %1$s was modified, do you want to upload it?
The file %1$s was saved with success!
- Create a new account
- Create a new account to start.
- Type
- Send feedback
- Copy URL
- Cut
- paste
- Advanced
- Auto
- Bytes
- Unit of measurement for file size
- Open Source licenses
- Show open source licenses
Open a file
- Open this time only
- Change the list type
+ No
diff --git a/libraries/sharedCode/src/main/res/values/strings_dialogs.xml b/libraries/sharedCode/src/main/res/values/strings_dialogs.xml
new file mode 100644
index 0000000..c9a4647
--- /dev/null
+++ b/libraries/sharedCode/src/main/res/values/strings_dialogs.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ Close
+
+
+ About
+ %1$s v%2$s
+ ]]>
+ open source app.
+ Copyright 2013-2014 Vlad Mihalachi. All Rights Reserved.
+
+ Many thanks to all who
+ helped with translations or
+ donated to me.
+
+ If you want to send feedback here is the XDA thread
+ ]]>
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/values/strings_donation.xml b/libraries/sharedCode/src/main/res/values/strings_donation.xml
new file mode 100644
index 0000000..c3b062f
--- /dev/null
+++ b/libraries/sharedCode/src/main/res/values/strings_donation.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+ Donate
+ Donate to developer
+ open source app.
+ You can show your appreciation and support development by donating:
+ ]]>
+
+ ﹩%1$s
+ You\'ve donated for this item already.
+
+ An ice cream
+ Cup of coffee
+ Electricity bills
+ The right pillow
+ Solid-state drive
+ Sound system
+
+ Failed to setup in-app-billing service!
+ Notice that Google is not responsible for that payments method.
+
+ PayPal
+
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/values/strings_turbo_client.xml b/libraries/sharedCode/src/main/res/values/strings_turbo_client.xml
new file mode 100644
index 0000000..4bbf011
--- /dev/null
+++ b/libraries/sharedCode/src/main/res/values/strings_turbo_client.xml
@@ -0,0 +1,104 @@
+
+
+
+
+ Change the list type
+ Copy URL
+ Cut
+ Paste
+ Advanced
+ Auto
+ Bytes
+ Unit of measurement for file size
+ Create a new account
+ Create a new account to start.
+ Type
+ The file %1$s was modified, do you want to upload it?
+ Sync
+ Remote folder to sync
+ Local folder to sync
+ Rate
+ Backup the accounts
+ Restore the accounts
+ Backup and share the accounts
+ Importing the accounts…
+ Exporting the accounts…
+ No backups found
+ Cannot open the file
+ Temporary folder does not exist
+ An error occurred
+ UI
+ Remove
+ Modification date
+ Name
+ Size
+ Sort
+ Dark
+ Select
+ Select an account
+ Are you sure?
+ Something failed
+ Do not transfer same file
+ App theme
+ Connection type
+ Protocol type
+ Another folder
+ Use a passphrase
+ Upload
+ Upload completed
+ What do you want to do?
+ Remote
+ To change the theme, restart the application
+ Rename
+ Default remote folder
+ Username
+ Passive
+ Passphrase
+ Password
+ Leave it empty to prompt for it every session
+ Port
+ Local
+ Logging in…
+ Edit
+ Move
+ Hide
+ Turbo Client
+ Host
+ New local folder
+ New remote folder
+ New remote file
+ New local file
+ Disconnect
+ Default local folder
+ Where to download?
+ Download
+ Download completed
+ Duplicate
+ Done
+ Home
+ Light
+ New account
+ Active
+ Delete
+ Deleting files…
+ Loading…
+ Current local folder
+ Private Key
+
\ No newline at end of file
diff --git a/libraries/sharedCode/src/main/res/values/styles.xml b/libraries/sharedCode/src/main/res/values/styles.xml
index 03d1a68..b62e93b 100644
--- a/libraries/sharedCode/src/main/res/values/styles.xml
+++ b/libraries/sharedCode/src/main/res/values/styles.xml
@@ -19,14 +19,14 @@
-
-
+
+ - @style/TextAppearance.Dialog.Title
+ - @style/TextAppearance.Dialog.Message
+ - @style/TextAppearance.Dialog.Info
-
-
-
+
+
+
+
+
+