diff --git a/QPythonOhBuildToolForAndroidStudio.7z b/QPythonOhBuildToolForAndroidStudio.7z new file mode 100644 index 0000000000000000000000000000000000000000..3657d8c2a5cdce4e2b9f262ec975cdcb8bd3df56 Binary files /dev/null and b/QPythonOhBuildToolForAndroidStudio.7z differ diff --git a/build.gradle b/build.gradle index 12d1b6462e312e7dab0fa919ebc4043f72a8fd21..111770191003264044295948916ec5fbf66661cd 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,7 @@ ext { minSdkVersion = 24 targetSdkVersion = 30 compileSdkVersion = 34 - buildToolsVersion = '30.0.2'//30.0.2正常 + buildToolsVersion = '30.0.3'//30.0.3正常 libOkHttp3 = "com.squareup.okhttp3:okhttp:${okhttpVersion}" diff --git a/local.properties b/local.properties index 2572451f1f77abd5660575cd121a29653e901287..ef17a15833b52f3b3129d1c89b5cc597ccd0ac04 100644 --- a/local.properties +++ b/local.properties @@ -1,8 +1,8 @@ -## This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. -# -# Location of the SDK. This is only used by Gradle. -# For customization when using a Version Control System, please read the -# header note. -#Mon Apr 18 21:47:10 CST 2022 -sdk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Thu Oct 05 20:11:09 CST 2023 +sdk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk diff --git a/qpypluginman/src/main/res/values-zh-rTW/strings.xml b/qpypluginman/src/main/res/values-zh-rTW/strings.xml deleted file mode 100644 index bc9f6cf0c4549d18746cd8604287bb6668eb581b..0000000000000000000000000000000000000000 --- a/qpypluginman/src/main/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - 檢查更新失敗 - 已經更新 - 提示 - 有可使用的新版本 - 更新 - 顯示詳細內容 - 取消 - 正在下載 - 已下載 - diff --git a/qpysdk/src/main/java/org/qpython/qpysdk/QPyConstants.java b/qpysdk/src/main/java/org/qpython/qpysdk/QPyConstants.java index 18d9114229632d3d6ea8e4ac8a6149300f0789bd..9e0af9b92175ca24b50c4d853817cda45852c79c 100644 --- a/qpysdk/src/main/java/org/qpython/qpysdk/QPyConstants.java +++ b/qpysdk/src/main/java/org/qpython/qpysdk/QPyConstants.java @@ -38,8 +38,8 @@ public interface QPyConstants extends BASE_CONF { String PYTHON_2 = "2.x"; String PyVer = "3.11"; - String QPYC3 = "https://dl.qpy.io/py3.json"; - String QPYC2COMPATIBLE = "https://dl.qpy.io/py2compatible.json"; + String QPYC3 = "https://io.qpython.org/py3.json"; + String QPYC2COMPATIBLE = "https://io.qpython.org/py2compatible.json"; String QPYC3_VER_KEY= "setting.py3resource.ver"; String QPYC2COMPATIBLE_VER_KEY="setting.py2compatibleresource.ver"; } diff --git a/qpython/build.gradle b/qpython/build.gradle index b8a653d232ac5940a297b00d2e562d397afda530..224141859ee3b60fa53e962b386597c544ea9a33 100644 --- a/qpython/build.gradle +++ b/qpython/build.gradle @@ -15,8 +15,8 @@ android { defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 324 - versionName "3.2.4" + versionCode 325 + versionName "3.2.5" multiDexEnabled true vectorDrawables.useSupportLibrary = true @@ -109,18 +109,6 @@ android { // resValue "string", "app_name", "Qpython" applicationId "org.qpython.qpy" } - os { -// resValue "string", "app_name", "Qpython" - applicationId "org.qpython.qpy" - } - op { -// resValue "string", "app_name", "QpythonL" - applicationId "com.hipipal.qpyplus" - } - oh { -// resValue "string", "app_name", "QpythonL" - applicationId "com.hipipal.qpyplus" - } } diff --git a/qpython/libs/arm64-v8a/lib-7z-elf.so b/qpython/libs/arm64-v8a/lib-7z-elf.so deleted file mode 100644 index d8943b67f9f991c56d22b2abbbc237671fa459fd..0000000000000000000000000000000000000000 Binary files a/qpython/libs/arm64-v8a/lib-7z-elf.so and /dev/null differ diff --git a/qpython/libs/arm64-v8a/lib-7z-txt.so b/qpython/libs/arm64-v8a/lib-7z-txt.so deleted file mode 100644 index 605d0d58f24febf50cbe8d6ecbd7409c708e1f97..0000000000000000000000000000000000000000 --- a/qpython/libs/arm64-v8a/lib-7z-txt.so +++ /dev/null @@ -1 +0,0 @@ -"$HOME/lib/p7zip/7z" "$@" \ No newline at end of file diff --git a/qpython/libs/arm64-v8a/lib-qpython3-setup.so b/qpython/libs/arm64-v8a/lib-qpython3-setup.so index ff4f7a4e08e4a5ada3481e5e0149e01d9562fb0f..ebcbb3b28006817abf3923edd39c3bc3f5c3691b 100644 --- a/qpython/libs/arm64-v8a/lib-qpython3-setup.so +++ b/qpython/libs/arm64-v8a/lib-qpython3-setup.so @@ -1,40 +1,48 @@ #!/system/bin/sh -BIN=$HOME/bin - -if [ "$1" == "Setup" ]; then -ln -s -f $ANDROID_NATIVE_LIBRARY/lib-qpython3-setup.so $BIN/qpython3.sh -$BIN/qpython3.sh setup +if [ "$1" != "setup" ]; then exit fi -if [ "$1" != "setup" ]; then -exit +if [ $ANDROID_SDK -ge 29 ]; +then +LNK=/system/bin/linker64 +else +LNK= fi +BIN=$HOME/bin + for i in python python3 python3.11 do -ln -s $ANDROID_NATIVE_LIBRARY/lib-python.so $BIN/$i +ln -s -f $ANDROID_NATIVE_LIBRARY/lib-python.so $BIN/$i done -for i in busybox bash sh +for i in busybox bash ash sh do -ln -s $ANDROID_NATIVE_LIBRARY/lib-busybox.so $BIN/$i +ln -s -f $ANDROID_NATIVE_LIBRARY/lib-busybox.so $BIN/$i done -i=$ANDROID_NATIVE_LIBRARY/lib-7z -ln -s $i-elf.so $HOME/lib/p7zip/7z -ln -s $i-txt.so $BIN/7z +function p7z { +j=$HOME/lib/p7zip/$1 +echo "\"$j\" \"\$@\"" > $BIN/$1 +chmod 777 $j +j="$LNK $j" +} + +p7z 7za for i in 1 2 do i=$HOME/private$i.7z -. 7z x $i -aoa -o$HOME +$j x $i -aoa -o$HOME rm $i done +p7z 7z + i=$HOME/public.7z -. 7z x $i -aoa -o$ANDROID_PUBLIC +$j x $i -aoa -o$ANDROID_PUBLIC rm $i for i in $(ls $BIN) @@ -45,4 +53,3 @@ done python $HOME/setup.py sleep 3 -exit diff --git a/qpython/src/main/AndroidManifest.xml b/qpython/src/main/AndroidManifest.xml index cf8892bd8312753a60bd7c75a20d3c591e991b57..f43df68fc8b218718dc628d5605cedd75fab2154 100644 --- a/qpython/src/main/AndroidManifest.xml +++ b/qpython/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ + @@ -50,11 +51,11 @@ - + + @@ -438,7 +439,6 @@ - @@ -469,14 +469,14 @@ android:permission="android.permission.BIND_JOB_SERVICE" android:process=":pushservice" /> - + + @@ -502,7 +502,7 @@ - + @@ -513,7 +513,7 @@ - + + + + + + + + + + + \ No newline at end of file diff --git a/qpython/src/main/assets/resource3.mp3 b/qpython/src/main/assets/resource3.mp3 index 73aad3dd6aa69127bca7941447c91c4503e0e7d8..c92e563aa64782d272c35abb240271c4fa7c93f3 100644 Binary files a/qpython/src/main/assets/resource3.mp3 and b/qpython/src/main/assets/resource3.mp3 differ diff --git a/qpython/src/ol/java/org/qpython/qpy/codeshare/ShareCodeUtil.java b/qpython/src/main/java/org/qpython/qpy/codeshare/ShareCodeUtil.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/codeshare/ShareCodeUtil.java rename to qpython/src/main/java/org/qpython/qpy/codeshare/ShareCodeUtil.java diff --git a/qpython/src/ol/java/org/qpython/qpy/codeshare/pojo/CloudFile.java b/qpython/src/main/java/org/qpython/qpy/codeshare/pojo/CloudFile.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/codeshare/pojo/CloudFile.java rename to qpython/src/main/java/org/qpython/qpy/codeshare/pojo/CloudFile.java diff --git a/qpython/src/main/java/org/qpython/qpy/console/ScriptExec.java b/qpython/src/main/java/org/qpython/qpy/console/ScriptExec.java index d9ca1b48ffa2936a23c5ad43fa42fd87ae4402b1..c8fe5ca9e64c0b474381df1a08fdcfca954257f2 100644 --- a/qpython/src/main/java/org/qpython/qpy/console/ScriptExec.java +++ b/qpython/src/main/java/org/qpython/qpy/console/ScriptExec.java @@ -139,7 +139,7 @@ public class ScriptExec { public String[] getPyEnv(Context context, String path, String term, String pyPath) { - boolean isQPy3 = NAction.isQPy3(context); + //boolean isQPy3 = NAction.isQPy3(context); File filesDir = context.getFilesDir(); File externalStorage = new File(FileUtils.getAbsolutePath(context)); @@ -177,7 +177,6 @@ public class ScriptExec { externalStorage.mkdir(); } - if (isQPy3) { String pyVer = QPyConstants.PyVer; env[5] = "PYTHONPATH=" +filesDir+"/lib/python"+pyVer+"/site-packages/:" @@ -192,22 +191,6 @@ public class ScriptExec { env[14] = "PYTHONSTARTUP="+filesDir+"/lib/python"+pyVer+"/site-packages/qpy.py"; - } else { - - env[5] = "PYTHONPATH=" - +filesDir+"/lib/python2.7/site-packages/:" - +filesDir+"/lib/python2.7/:" - +filesDir+"/lib/python27.zip:" - //+filesDir+"/lib/notebook.zip:" - +filesDir+"/lib/python2.7/qpyutil.zip:" - +filesDir+"/lib/python2.7/lib-dynload/:" - +externalStorage+"/lib/python2.7/site-packages/:" - +pyPath; - - //env[14] = "IS_QPY2=1"; - env[14] = "PYTHONSTARTUP="+filesDir+"/lib/python2.7/site-packages/qpy.py"; - } - env[6] = "PYTHONOPTIMIZE=0"; //乘着船:修复__doc__为None File td = new File(externalStorage+"/cache"); @@ -225,15 +208,16 @@ public class ScriptExec { env[12] = "ANDROID_NATIVE_LIBRARY="+ CONF.NATIVE_LIBRARY; env[13] = "ANDROID_ARGUMENT=\""+pyPath+"\""; - env[15] = "QPY_USERNO="+ NAction.getUserNoId(context); + //env[15] = "QPY_USERNO="+ NAction.getUserNoId(context); env[16] = "QPY_ARGUMENT="+NAction.getExtConf(context); env[17] = "PYTHONDONTWRITEBYTECODE=1"; env[18] = "TMP="+externalStorage+"/cache"; env[19] = "ANDROID_APP_PATH="+externalStorage+""; - env[20] = "LANG=en_US.UTF-8"; + env[20] = "LANG="+context.getString(R.string.lang_env)+".UTF-8"; env[21] = "HOME="+context.getFilesDir(); env[22] = "ANDROID_DATA="+System.getenv("ANDROID_DATA"); env[23] = "ANDROID_ROOT="+System.getenv("ANDROID_ROOT"); + env[15] = "ANDROID_SDK="+Build.VERSION.SDK_INT; return env; } @@ -241,25 +225,13 @@ public class ScriptExec { String scmd = ""; String v = NAction.isQPy3(context) ? "3" : ""; boolean isRootEnable = NAction.isRootEnable(context); - if (Build.VERSION.SDK_INT >= 20) { - if (bin) { - scmd = context.getFilesDir() + "/bin/python" + v ;//+ "-android5"; - } else { - if (isRootEnable) { - scmd = context.getFilesDir() + "/bin/qpython" + v +"-root.sh";//+ "-android5-root.sh"; - } else { - scmd = context.getFilesDir() + "/bin/qpython" + v + ".sh";//+ "-android5.sh"; - } - } + if (bin) { + scmd = context.getFilesDir() + "/bin/python" + v ;//+ "-android5"; } else { - if (bin) { - scmd = context.getFilesDir() + "/bin/python" + v; + if (isRootEnable) { + scmd = context.getFilesDir() + "/bin/qpython" + v +"-root.sh";//+ "-android5-root.sh"; } else { - if (isRootEnable) { - scmd = context.getFilesDir() + "/bin/qpython" + v + "-root.sh"; - } else { - scmd = context.getFilesDir() + "/bin/qpython" + v + ".sh"; - } + scmd = context.getFilesDir() + "/bin/qpython" + v + ".sh";//+ "-android5.sh"; } } return scmd; @@ -459,68 +431,23 @@ public class ScriptExec { context.startActivityForResult(intent, SCRIPT_CONSOLE_CODE); } - -/* public void sendLogNotifiy(Context context, String filePath) { - if (!PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.key_hide_noti), true)) { - return; - } - String title = FileHelper.getFileName(filePath); - - Intent resultIntent = new Intent(context, LogActivity.class); - if (filePath.contains("/scripts/")) { - String proj = new File(filePath).getName(); - - resultIntent.putExtra(LogActivity.LOG_PATH, filePath.replace(title, "last.log")); - resultIntent.putExtra(LogActivity.LOG_TITLE, proj); - } else { - String proj = new File(filePath).getParentFile().getName(); - resultIntent.putExtra(LogActivity.LOG_PATH, filePath.replace("main.py", "last.log")); - resultIntent.putExtra(LogActivity.LOG_TITLE, proj); - } - - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_ONE_SHOT); - -// PendingIntent resultPendingIntent = PendingIntent.getActivity( -// this, 0, resultIntent, 0); - - Notification.Builder builder = new Notification.Builder(context) - .setSmallIcon(R.drawable.img_home_logo) - .setContentTitle(context.getString(R.string.app_name)) - .setContentText(context.getString(R.string.runtime_log, title)) - .setContentIntent(pendingIntent) - .setAutoCancel(true); - - Notification notification; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - notification = builder.build(); - } else { - notification = builder.getNotification(); - } - NotificationManager mNotifyMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - mNotifyMgr.notify(LOG_NOTIFICATION_ID, notification); - } -*/ public final void checkPermissionDo(Context context, String[] permissions, PermissionAction action) { Map mActionMap = new ArrayMap<>(); - if (Build.VERSION.SDK_INT >= 23) { - boolean granted = true; - for (int i = 0; i < permissions.length; i++) { - String permission = permissions[i]; - int checkPermission = ContextCompat.checkSelfPermission(context, permission); - if (checkPermission != PackageManager.PERMISSION_GRANTED) { - granted = false; - - } else { - granted = true; - } - } - if (!granted) { - int code = permissions.hashCode() & 0xffff; - mActionMap.put(code, action); - ActivityCompat.requestPermissions((Activity) context, permissions, code); + boolean granted = true; + for (int i = 0; i < permissions.length; i++) { + String permission = permissions[i]; + int checkPermission = ContextCompat.checkSelfPermission(context, permission); + if (checkPermission != PackageManager.PERMISSION_GRANTED) { + granted = false; + } else { - action.onGrant(); + granted = true; } + } + if (!granted) { + int code = permissions.hashCode() & 0xffff; + mActionMap.put(code, action); + ActivityCompat.requestPermissions((Activity) context, permissions, code); } else { action.onGrant(); } diff --git a/qpython/src/main/java/org/qpython/qpy/console/TermActivity.java b/qpython/src/main/java/org/qpython/qpy/console/TermActivity.java index eec07fac0e0ceb589743218892eab442abd567d2..f548b75319825e9c51a9038bc349a8385d377d94 100644 --- a/qpython/src/main/java/org/qpython/qpy/console/TermActivity.java +++ b/qpython/src/main/java/org/qpython/qpy/console/TermActivity.java @@ -31,7 +31,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.net.Uri; import android.net.wifi.WifiManager; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -69,7 +68,6 @@ import com.quseit.util.NAction; import org.qpython.qpy.R; import org.qpython.qpy.console.compont.ActivityCompat; -import org.qpython.qpysdk.utils.AndroidCompat; import org.qpython.qpy.console.compont.FileCompat; import org.qpython.qpy.console.compont.MenuItemCompat; import org.qpython.qpy.console.util.SessionList; @@ -78,6 +76,7 @@ import org.qpython.qpy.main.activity.NotebookActivity; import org.qpython.qpy.main.app.App; import org.qpython.qpy.texteditor.ui.adapter.bean.PopupItemBean; import org.qpython.qpy.texteditor.ui.view.EditorPopUp; +import org.qpython.qpysdk.utils.AndroidCompat; import org.qpython.qsl4a.qsl4a.StringUtils; import java.io.File; @@ -85,7 +84,6 @@ import java.io.IOException; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -751,17 +749,9 @@ public class TermActivity extends AppCompatActivity implements UpdateCallback, S private void setTextToEmulator(String text) { if (mViewFlipper.getCurrentView() != null) { - if (text!=null && text.equals("[tab]")) { - Log.d("TermAcity", "setTextToEmulator:"+text); - - //((EmulatorView) mViewFlipper.getCurrentView()).sendTabKey(); - - ((EmulatorView) mViewFlipper.getCurrentView()).setTextBuff("\t"); - ((EmulatorView) mViewFlipper.getCurrentView()).getTermSession().write("\t"); - ((EmulatorView) mViewFlipper.getCurrentView()).getTermSession().notifyUpdate(); - - - } else { + { + if(text.equals("[tab]")) + text="\t"; ((EmulatorView) mViewFlipper.getCurrentView()).setTextBuff(text); ((EmulatorView) mViewFlipper.getCurrentView()).getTermSession().write(text); ((EmulatorView) mViewFlipper.getCurrentView()).getTermSession().notifyUpdate(); @@ -1171,7 +1161,7 @@ public class TermActivity extends AppCompatActivity implements UpdateCallback, S if (shell_type!=null){ if (shell_type.equals("shell")) - session = createTermSession(this, settings, "cd $HOME && cat SHELL", this.getFilesDir().getAbsolutePath()); + session = createTermSession(this, settings, settings.getShellInterface(), this.getFilesDir().getAbsolutePath()); else if (shell_type.equals("setup")) { session = setup(settings); } @@ -1222,9 +1212,10 @@ public class TermActivity extends AppCompatActivity implements UpdateCallback, S pref.putString("shell",getString(R.string.pref_shell_default)).apply(); String filesDir = this.getFilesDir().getAbsolutePath(); TermSession session = createTermSession(this, settings, - this.getApplicationInfo().nativeLibraryDir + "/lib-qpython3-setup.so Setup", + this.getApplicationInfo().nativeLibraryDir + "/lib-qpython3-setup.so setup && exit", filesDir); pref.putString("shell",filesDir+"/bin/bash").apply(); + this.finish(); return session; } diff --git a/qpython/src/main/java/org/qpython/qpy/console/util/TermSettings.java b/qpython/src/main/java/org/qpython/qpy/console/util/TermSettings.java index ca46dcc0cb9ac0619caf4caa5b7e3801d14dfe20..a51a429801a081fcbf70d672274f2a7edc9fa1f1 100644 --- a/qpython/src/main/java/org/qpython/qpy/console/util/TermSettings.java +++ b/qpython/src/main/java/org/qpython/qpy/console/util/TermSettings.java @@ -42,6 +42,7 @@ public class TermSettings { private int mFnKeyId; private int mUseCookedIME; private String mShell; + private String mShellInterface; private String mFailsafeShell; private String mInitialCommand; private String mTermType; @@ -71,6 +72,7 @@ public class TermSettings { private static final String FNKEY_KEY = "fnkey"; private static final String IME_KEY = "ime"; private static final String SHELL_KEY = "shell"; + private static final String SHELL_INTERFACE_KEY = "shell_interface"; private static final String INITIALCOMMAND_KEY = "initialcommand"; private static final String TERMTYPE_KEY = "termtype"; private static final String CLOSEONEXIT_KEY = "close_window_on_process_exit"; @@ -114,9 +116,9 @@ public class TermSettings { public static final int ACTION_BAR_MODE_HIDES = 2; private static final int ACTION_BAR_MODE_MAX = 2; - public static final int ORIENTATION_UNSPECIFIED = 0; + /*public static final int ORIENTATION_UNSPECIFIED = 0; public static final int ORIENTATION_LANDSCAPE = 1; - public static final int ORIENTATION_PORTRAIT = 2; + public static final int ORIENTATION_PORTRAIT = 2;*/ /** * An integer not in the range of real key codes. @@ -174,6 +176,7 @@ public class TermSettings { mUseCookedIME = Integer.parseInt(res.getString(R.string.pref_ime_default)); mFailsafeShell = res.getString(R.string.pref_shell_default); mShell = mFailsafeShell; + mShellInterface = res.getString(R.string.pref_shell_interface_default); mInitialCommand = res.getString(R.string.pref_initialcommand_default_qpython); mTermType = res.getString(R.string.pref_termtype_default); mCloseOnExit = res.getBoolean(R.bool.pref_close_window_on_process_exit_default); @@ -203,6 +206,7 @@ public class TermSettings { FN_KEY_SCHEMES.length - 1); mUseCookedIME = readIntPref(IME_KEY, mUseCookedIME, 1); mShell = readStringPref(SHELL_KEY, mShell); + mShellInterface = readStringPref(SHELL_INTERFACE_KEY,mShellInterface); mInitialCommand = readStringPref(INITIALCOMMAND_KEY, mInitialCommand); mTermType = readStringPref(TERMTYPE_KEY, mTermType); mCloseOnExit = readBooleanPref(CLOSEONEXIT_KEY, mCloseOnExit); @@ -324,6 +328,10 @@ public class TermSettings { return mShell; } + public String getShellInterface() { + return mShellInterface; + } + public String getFailsafeShell() { return mFailsafeShell; } diff --git a/qpython/src/ol/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java rename to qpython/src/main/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java diff --git a/qpython/src/main/java/org/qpython/qpy/main/activity/GistDetailActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/GistDetailActivity.java index 22206414c757d4f37d211c6731cf0b9fde9abf23..84701344b9bf80130b06cc8ebff49fe2319989eb 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/activity/GistDetailActivity.java +++ b/qpython/src/main/java/org/qpython/qpy/main/activity/GistDetailActivity.java @@ -148,7 +148,7 @@ public class GistDetailActivity extends BaseActivity implements DetailView { break; } case ShareDialog.COPY_LINK: { - OpenWebUtil.open(this, "http://gist.qpy.io/share/"+getIntent().getStringExtra(GIST_ID)); + OpenWebUtil.open(this, "http://gist.qpython.org/share/"+getIntent().getStringExtra(GIST_ID)); break; } } diff --git a/qpython/src/main/java/org/qpython/qpy/main/activity/HomeMainActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/HomeMainActivity.java index 33e53577154c93dbbb3b3aeb91e6b140b21046e9..036e481a705ed66608db8a10c22c1998225d41cd 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/activity/HomeMainActivity.java +++ b/qpython/src/main/java/org/qpython/qpy/main/activity/HomeMainActivity.java @@ -4,6 +4,7 @@ import static org.qpython.qpysdk.QPyConstants.PYTHON_2; import android.Manifest; import android.annotation.SuppressLint; +import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -12,14 +13,15 @@ import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; import android.net.Uri; import android.os.Bundle; +import android.os.CountDownTimer; import android.os.Handler; import android.os.Message; +import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AlertDialog; import android.text.Html; import android.text.TextUtils; -import android.util.Log; import android.view.View; import android.widget.Toast; @@ -48,7 +50,7 @@ import org.qpython.qpysdk.QPyConstants; import org.qpython.qpysdk.QPySDK; import org.qpython.qpysdk.utils.FileHelper; import org.qpython.qsl4a.qsl4a.facade.AndroidFacade; -import org.qpython.qsl4a.qsl4a.facade.QPyInterfaceFacade; +import org.qpython.qsl4a.qsl4a.facade.QPyInterfaceFacade;; import java.io.File; import java.util.Map; @@ -352,76 +354,28 @@ public class HomeMainActivity extends BaseActivity { } } -// private void initCourseListener() { -// App.getService().getCourseAd().subscribeOn(Schedulers.io()) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe(new MySubscriber() { -// @Override -// public void onNext(CourseAdModel o) { -// super.onNext(o); -// if ("0".equals(o.getQpy().getCourse_open())) { -// // open web -// binding.llCourse.setOnClickListener(v -> -// QWebViewActivity.start(HomeMainActivity.this, getString(R.string.course), URL_COURSE)); -// } else { -// // open with native -// binding.llCourse.setOnClickListener(v -> { -// CourseActivity.start(HomeMainActivity.this); -// sendEvent(getString(R.string.event_course)); -// }); -// } -// } -// -// @Override -// public void onError(Throwable e) { -// binding.llCourse.setOnClickListener(v -> -// QWebViewActivity.start(HomeMainActivity.this, getString(R.string.course), URL_COURSE)); -// } -// }); -// } - @Override protected void onPause() { super.onPause(); } -// private void startPyService() { -// Log.d(TAG, "startPyService"); -// Intent intent = new Intent(this, QPyScriptService.class); -// startService(intent); -// } - private void openQpySDK() { - Log.d("HomeMainActivity", "openQpySDK"); + /*Log.d("HomeMainActivity", "openQpySDK"); String[] permssions = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; checkPermissionDo(permssions, new BaseActivity.PermissionAction() { @Override - public void onGrant() { + public void onGrant() {*/ //这里只执行一次做为初始化 if (!NAction.isQPyInterpreterSet(HomeMainActivity.this)) { - /*new AlertDialog.Builder(HomeMainActivity.this, R.style.MyDialog) - .setTitle(R.string.notice) - .setMessage(R.string.py2_or_3) - .setPositiveButton(R.string.use_py3, (dialog1, which) - -> { - initQpySDK3(clickListener); - }) - .setNegativeButton(R.string.use_py2, (dialog1, which) - -> { - initQpySDK(clickListener); - }) - .create() - .show();*/ initQpySDK3(); - } //clickListener.onClick(null); - - } + } + /*} @Override public void onDeny() { Toast.makeText(HomeMainActivity.this, getString(R.string.grant_storage_hint), Toast.LENGTH_SHORT).show(); } - }); + });*/ try { CONF.NATIVE_LIBRARY = this.getPackageManager().getApplicationInfo( this.getPackageName(),PackageManager.GET_UNINSTALLED_PACKAGES).nativeLibraryDir; @@ -434,17 +388,41 @@ public class HomeMainActivity extends BaseActivity { * 在工作线程中作初始化 */ private void initQpySDK3() { - Log.d(TAG, "initQpySDK3"); + long t1 = SystemClock.elapsedRealtimeNanos(); NAction.setQPyInterpreter(HomeMainActivity.this, "3.x"); initQPy(); initIcon(); + long t2 = SystemClock.elapsedRealtimeNanos(); + int t = (int) Math.round((t2-t1)*0.025); + File f = new File(getFilesDir()+"/resource3.version"); + ProgressDialog progressDialog = new ProgressDialog(this); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setMax(t); + progressDialog.setTitle(R.string.initial_qpython); + progressDialog.setCancelable(false); + progressDialog.show(); + final boolean[] exist = {false}; + (new CountDownTimer(t,1000){ + @Override + public void onTick(long l) { + int c = t - (int) l; + progressDialog.setProgress(c); + if(c*4 { diff --git a/qpython/src/main/java/org/qpython/qpy/main/activity/NotebookActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/NotebookActivity.java index 8fbf16079cebd6050c919843a806413ad0a23247..14de84c3809705dc37b76a569fe3128b6af90e7a 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/activity/NotebookActivity.java +++ b/qpython/src/main/java/org/qpython/qpy/main/activity/NotebookActivity.java @@ -179,8 +179,7 @@ public class NotebookActivity extends BaseActivity implements View.OnClickListen if (filePath==null || !filePath.contains(NotebookUtil.NOTEBOOK_DIR)) { return ""; } - String path = filePath.substring(NotebookUtil.NOTEBOOK_DIR.length())+""; - return path; + return filePath.substring(NotebookUtil.NOTEBOOK_DIR.length())+""; } private void initListener() { @@ -210,7 +209,7 @@ public class NotebookActivity extends BaseActivity implements View.OnClickListen lastNotebook = mSharedPreferences.getString("last_notebook", null); if (lastNotebook == null) { String fileName = "Welcome.ipynb"; - File file = new File(NotebookUtil.NOTEBOOK_DIR + "notebooks", fileName); + File file = new File(NotebookUtil.NOTEBOOK_DIR , fileName); if (file.exists()) { lastNotebook = file.getAbsolutePath(); } else { @@ -423,7 +422,7 @@ public class NotebookActivity extends BaseActivity implements View.OnClickListen public boolean OnClickListener(String name) { doAction(NoteBookAction.SAVE_NOTEBOOK); File file = new File(lastNotebook); - File newFile = new File(NotebookUtil.NOTEBOOK_DIR + "notebooks/", name + NotebookUtil.ext); + File newFile = new File(NotebookUtil.NOTEBOOK_DIR , name + NotebookUtil.ext); file.renameTo(newFile); lastNotebook = newFile.getAbsolutePath(); mBinding.toolbar.setTitle(getFileName(lastNotebook)); diff --git a/qpython/src/ol/java/org/qpython/qpy/main/activity/PayActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/PayActivity.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/activity/PayActivity.java rename to qpython/src/main/java/org/qpython/qpy/main/activity/PayActivity.java diff --git a/qpython/src/ol/java/org/qpython/qpy/main/activity/PurchaseActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/PurchaseActivity.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/activity/PurchaseActivity.java rename to qpython/src/main/java/org/qpython/qpy/main/activity/PurchaseActivity.java diff --git a/qpython/src/main/java/org/qpython/qpy/main/activity/QrCodeActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/QrCodeActivity.java index ac7862849840660ca516439d33ee8362e7676902..219ecb13d4ea6078b021a56f0fd3cadc1cf4a256 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/activity/QrCodeActivity.java +++ b/qpython/src/main/java/org/qpython/qpy/main/activity/QrCodeActivity.java @@ -84,7 +84,7 @@ public class QrCodeActivity extends AppCompatActivity implements ZXingScannerVie .show(); } else if (scanResult.startsWith("qwe://")) { - // start the qpy.io editor + // start the qpython editor if (scanResult.contains("token=")) { String qweLink = scanResult.replace("qwe://", "http://") + "&ip=" + NUtil.getIPAddress(true) + ":10000"; Map> ps = NUtil.getQueryParams(qweLink); diff --git a/qpython/src/ol/java/org/qpython/qpy/main/activity/SignInActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/SignInActivity.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/activity/SignInActivity.java rename to qpython/src/main/java/org/qpython/qpy/main/activity/SignInActivity.java diff --git a/qpython/src/ol/java/org/qpython/qpy/main/activity/UserActivity.java b/qpython/src/main/java/org/qpython/qpy/main/activity/UserActivity.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/activity/UserActivity.java rename to qpython/src/main/java/org/qpython/qpy/main/activity/UserActivity.java diff --git a/qpython/src/ol/java/org/qpython/qpy/main/app/AppInit.java b/qpython/src/main/java/org/qpython/qpy/main/app/AppInit.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/app/AppInit.java rename to qpython/src/main/java/org/qpython/qpy/main/app/AppInit.java diff --git a/qpython/src/ol/java/org/qpython/qpy/main/fragment/ExplorerFragment.java b/qpython/src/main/java/org/qpython/qpy/main/fragment/ExplorerFragment.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/fragment/ExplorerFragment.java rename to qpython/src/main/java/org/qpython/qpy/main/fragment/ExplorerFragment.java diff --git a/qpython/src/ol/java/org/qpython/qpy/main/fragment/MyProjectFragment.java b/qpython/src/main/java/org/qpython/qpy/main/fragment/MyProjectFragment.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/fragment/MyProjectFragment.java rename to qpython/src/main/java/org/qpython/qpy/main/fragment/MyProjectFragment.java diff --git a/qpython/src/main/java/org/qpython/qpy/main/fragment/SettingFragment.java b/qpython/src/main/java/org/qpython/qpy/main/fragment/SettingFragment.java index c62a2ff25ff6540179b9a8724aa8f78c35a650c9..fab1c60a6055d051f234860df2bf7ab557733e07 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/fragment/SettingFragment.java +++ b/qpython/src/main/java/org/qpython/qpy/main/fragment/SettingFragment.java @@ -523,7 +523,7 @@ public class SettingFragment extends PreferenceFragment { private void releaseNotebook(Preference preference) { Observable.create((Observable.OnSubscribe) subscriber -> { try { - String nbfile = NStorage.getSP(App.getContext(), NotebookUtil.getNbResFk(getActivity())); + String nbfile = NStorage.getSP(App.getContext(), NotebookUtil.getNbResFk()); if (!nbfile.equals("") && new File(nbfile).exists()) { // extractNotebookRes(nbfile); } @@ -607,7 +607,7 @@ public class SettingFragment extends PreferenceFragment { } } - private void releasePython2Standard(Preference preference) { + /*private void releasePython2Standard(Preference preference) { Observable.create((Observable.OnSubscribe) subscriber -> { try { //removeQPyc2Core(); @@ -647,9 +647,9 @@ public class SettingFragment extends PreferenceFragment { getActivity().recreate(); } }); - } + }*/ - private void releasePython2Compatable(Preference preference) { + /*private void releasePython2Compatable(Preference preference) { Observable.create((Observable.OnSubscribe) subscriber -> { try { @@ -688,7 +688,7 @@ public class SettingFragment extends PreferenceFragment { getActivity().recreate(); } }); - } + }*/ private void releasePython3(Preference preference) { @@ -815,7 +815,7 @@ public class SettingFragment extends PreferenceFragment { final String vername = result.getString("vername"); final String path = NotebookUtil.RELEASE_PATH + "/" + target; - NStorage.setSP(App.getContext(), NotebookUtil.getNbResFk(getActivity()), path); + NStorage.setSP(App.getContext(), NotebookUtil.getNbResFk(), path); Log.d(TAG, "getNotebook:onSuccess:" + notebook_resource_ver + "[" + vercode + "]"); diff --git a/qpython/src/ol/java/org/qpython/qpy/main/receiver/LeancloudReceiver.java b/qpython/src/main/java/org/qpython/qpy/main/receiver/LeancloudReceiver.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/receiver/LeancloudReceiver.java rename to qpython/src/main/java/org/qpython/qpy/main/receiver/LeancloudReceiver.java diff --git a/qpython/src/main/java/org/qpython/qpy/main/server/Service.java b/qpython/src/main/java/org/qpython/qpy/main/server/Service.java index 5076f105128c6fd24b17a861bd1ad7cda4ec31df..c2fc2b6f43dd8a0b002398a4f5d6c2bf6d9a24e0 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/server/Service.java +++ b/qpython/src/main/java/org/qpython/qpy/main/server/Service.java @@ -14,7 +14,6 @@ import com.quseit.util.FileUtils; import com.quseit.util.NAction; import org.greenrobot.eventbus.EventBus; -import org.qpython.qpy.BuildConfig; import org.qpython.qpy.main.activity.LibActivity; import org.qpython.qpy.main.app.App; import org.qpython.qpy.main.server.model.BaseLibModel; @@ -33,6 +32,7 @@ import java.util.List; import rx.Observable; import rx.Subscriber; +import rx.android.BuildConfig; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; @@ -45,7 +45,7 @@ public class Service extends CacheKey { public Service() { request = App.getRetrofit() - .baseUrl(BASE_URL + "dl.qpy.io") + .baseUrl(BASE_URL + "io.qpython.org") .build() .create(ServiceRequest.class); diff --git a/qpython/src/main/java/org/qpython/qpy/main/server/ServiceRequest.java b/qpython/src/main/java/org/qpython/qpy/main/server/ServiceRequest.java index 7dbdb26725139f19c760cfbda3ec7e0245347426..b4bfa66808df009a5a5d814c5ec440de8addd313 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/server/ServiceRequest.java +++ b/qpython/src/main/java/org/qpython/qpy/main/server/ServiceRequest.java @@ -24,7 +24,7 @@ import rx.Observable; interface ServiceRequest { /** - * Domain: http://dl.qpy.io + * Domain: http://io.qpython.org */ @GET("/libs-2x-715.json") @@ -36,13 +36,13 @@ interface ServiceRequest { @GET("/qpypi-2x-715.json") Observable> getQPyPi(); - @GET("/qpypi-3x-66.json") + @GET("/qpypi-3x-311.json") Observable> getQPyPi3(); @GET("/aipy-2x-715.json") Observable> getAIPy(); - @GET("/aipy-3x-66.json") + @GET("/aipy-3x-311.json") Observable> getAIPy3(); @GET("/update.json") diff --git a/qpython/src/main/java/org/qpython/qpy/main/server/model/LibModel.java b/qpython/src/main/java/org/qpython/qpy/main/server/model/LibModel.java index 4fc49fe86245100a8bbc22416871aa61658ec9ea..de2c05e2f312a3c8dd5ad00410f75d3222203a51 100644 --- a/qpython/src/main/java/org/qpython/qpy/main/server/model/LibModel.java +++ b/qpython/src/main/java/org/qpython/qpy/main/server/model/LibModel.java @@ -17,7 +17,7 @@ public class LibModel extends BaseLibModel{ /** * src : http://kivy.org * rdate : 2017-06-15 - * link : http://dl.qpy.io/2x/kivy/kivy-1.9.1.zip + * link : http://io.qpython.org/2x/kivy/kivy-1.9.1.zip * description : kivy 1.9.1 prebuild for qpython * title : kivy * ver : 1 diff --git a/qpython/src/ol/java/org/qpython/qpy/main/service/PayUtil.java b/qpython/src/main/java/org/qpython/qpy/main/service/PayUtil.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/main/service/PayUtil.java rename to qpython/src/main/java/org/qpython/qpy/main/service/PayUtil.java diff --git a/qpython/src/main/java/org/qpython/qpy/texteditor/TedAppWidgetProvider.java b/qpython/src/main/java/org/qpython/qpy/texteditor/TedAppWidgetProvider.java index 7774f9174e7a907d3a5e863f40ae7c9b3762eddf..18e7a0b6bdca4496e9e262292bcf9170d7e7cccb 100644 --- a/qpython/src/main/java/org/qpython/qpy/texteditor/TedAppWidgetProvider.java +++ b/qpython/src/main/java/org/qpython/qpy/texteditor/TedAppWidgetProvider.java @@ -1,11 +1,5 @@ package org.qpython.qpy.texteditor; -import java.io.File; - -import org.qpython.qpy.R; -import org.qpython.qpy.texteditor.common.Constants; -import org.qpython.qpy.texteditor.common.WidgetPrefs; - import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; @@ -15,7 +9,14 @@ import android.content.Intent; import android.net.Uri; import android.util.Log; import android.widget.RemoteViews; -import org.qpython.qpy.BuildConfig; + +import org.qpython.qpy.R; +import org.qpython.qpy.texteditor.common.Constants; +import org.qpython.qpy.texteditor.common.WidgetPrefs; + +import java.io.File; + +import rx.android.BuildConfig; public class TedAppWidgetProvider extends AppWidgetProvider implements Constants { diff --git a/qpython/src/ol/java/org/qpython/qpy/texteditor/TedLocalActivity.java b/qpython/src/main/java/org/qpython/qpy/texteditor/TedLocalActivity.java similarity index 100% rename from qpython/src/ol/java/org/qpython/qpy/texteditor/TedLocalActivity.java rename to qpython/src/main/java/org/qpython/qpy/texteditor/TedLocalActivity.java diff --git a/qpython/src/main/java/org/qpython/qpy/utils/NotebookUtil.java b/qpython/src/main/java/org/qpython/qpy/utils/NotebookUtil.java index f933ba76d2a7d486d20854b0c55c4a3536011b93..be40a24b1f62ac11e1eb41d3937fa55ffde22dbe 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/NotebookUtil.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/NotebookUtil.java @@ -43,9 +43,9 @@ public class NotebookUtil { public static final String RELEASE_PATH = FileUtils.getAbsolutePath(App.getContext()) + "/.notebook"; public static final String NB_SERVER = "http://127.0.0.1:13000"; public static final String KILL_SERVER = NB_SERVER + "/__exit"; - public static final String NOTEBOOK_SERVER = NB_SERVER + "/notebooks/"; + public static final String NOTEBOOK_SERVER = NB_SERVER + "/notebooks/files/notebooks"; - public static final String NOTEBOOK_DIR = Environment.getExternalStorageDirectory() +"/"; + public static final String NOTEBOOK_DIR = RELEASE_PATH.replace("/.notebook","/notebooks"); public static final String ext = ".ipynb"; public static final String Untitled = "Untitled"; @@ -87,7 +87,7 @@ public class NotebookUtil { } } } - if (context.getPackageName().equals("com.hipipal.qpyplus")) { // old version + /*if (context.getPackageName().equals("com.hipipal.qpyplus")) { // old version // Rename correct files to String[] list = {"jupyter-chq", "jupyter2-chq", "jupyter-notebook-chq", "jupyter2-notebook-chq"}; for (int i=0;iIt is possible that an in-app item may be acquired without the - * application calling getBuyIntent(), for example if the item can be - * redeemed from inside the Play Store using a promotional code. If this - * application isn't running at the time, then when it is started a call - * to getPurchases() will be sufficient notification. However, if the - * application is already running in the background when the item is acquired, - * a message to this BroadcastReceiver will indicate that the an item - * has been acquired.

- */ -public class IabBroadcastReceiver extends BroadcastReceiver { - /** - * The Intent action that this Receiver should filter for. - */ - public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED"; - private final IabBroadcastListener mListener; - - public IabBroadcastReceiver(IabBroadcastListener listener) { - mListener = listener; - } - - @Override - public void onReceive(Context context, Intent intent) { - if (mListener != null) { - mListener.receivedBroadcast(); - } - } - - /** - * Listener interface for received broadcast messages. - */ - public interface IabBroadcastListener { - void receivedBroadcast(); - } -} +/* Copyright (c) 2014 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 org.qpython.qpy.utils.iap; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Receiver for the "com.android.vending.billing.PURCHASES_UPDATED" Action + * from the Play Store. + * + *

It is possible that an in-app item may be acquired without the + * application calling getBuyIntent(), for example if the item can be + * redeemed from inside the Play Store using a promotional code. If this + * application isn't running at the time, then when it is started a call + * to getPurchases() will be sufficient notification. However, if the + * application is already running in the background when the item is acquired, + * a message to this BroadcastReceiver will indicate that the an item + * has been acquired.

+ */ +public class IabBroadcastReceiver extends BroadcastReceiver { + /** + * The Intent action that this Receiver should filter for. + */ + public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED"; + private final IabBroadcastListener mListener; + + public IabBroadcastReceiver(IabBroadcastListener listener) { + mListener = listener; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mListener != null) { + mListener.receivedBroadcast(); + } + } + + /** + * Listener interface for received broadcast messages. + */ + public interface IabBroadcastListener { + void receivedBroadcast(); + } +} diff --git a/qpython/src/main/java/org/qpython/qpy/utils/iap/IabException.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/IabException.java index ec66b0a69e62f038fdd9ab165a3c973449bae5f1..e2839b961165b7ec94e2263b97029521158d4d86 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/IabException.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/IabException.java @@ -1,43 +1,43 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -/** - * 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; } +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +/** + * 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/qpython/src/main/java/org/qpython/qpy/utils/iap/IabHelper.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/IabHelper.java index cc0c333479bb07f50c80e8a5c64915253c15af88..f75f0692ed0b0b2ca3125ea90243f0247e252146 100755 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/IabHelper.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/IabHelper.java @@ -1,1095 +1,1095 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -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 com.android.vending.billing.IInAppBillingService; - -import org.json.JSONException; - -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. It is invalid to - * dispose the object while an asynchronous operation is in progress. You can - * call {@link #disposeWhenFinished()} to ensure that any in-progress operation - * completes before the object is disposed. - * - * 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. - * - */ -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_SERVICE_UNAVAILABLE = 2; - 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; - public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011; - // 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"; - // Ensure atomic access to mAsyncInProgress and mDisposeAfterAsync. - private final Object mAsyncInProgressLock = new Object(); - // 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; - // Do we need to dispose this object after an in-progress asynchronous operation? - boolean mDisposeAfterAsync = false; - // Are subscriptions supported? - boolean mSubscriptionsSupported = false; - // Is subscription update supported? - boolean mSubscriptionUpdateSupported = 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; - mSubscriptionUpdateSupported = false; - return; - } else { - logDebug("In-app billing version 3 supported for " + packageName); - } - - // Check for v5 subscriptions support. This is needed for - // getBuyIntentToReplaceSku which allows for subscription update - response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS); - if (response == BILLING_RESPONSE_RESULT_OK) { - logDebug("Subscription re-signup AVAILABLE."); - mSubscriptionUpdateSupported = true; - } else { - logDebug("Subscription re-signup not available."); - mSubscriptionUpdateSupported = false; - } - - if (mSubscriptionUpdateSupported) { - mSubscriptionsSupported = true; - } else { - // 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); - mSubscriptionsSupported = false; - mSubscriptionUpdateSupported = false; - } - } - - 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 intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0); - if (intentServices != null && !intentServices.isEmpty()) { - // service available to handle that Intent - 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() throws IabAsyncInProgressException { - synchronized (mAsyncInProgressLock) { - if (mAsyncInProgress) { - throw new IabAsyncInProgressException("Can't dispose because an async operation " + - "(" + mAsyncOperation + ") is in progress."); - } - } - logDebug("Disposing."); - mSetupDone = false; - if (mServiceConn != null) { - logDebug("Unbinding from service."); - if (mContext != null) mContext.unbindService(mServiceConn); - } - mDisposed = true; - mContext = null; - mServiceConn = null; - mService = null; - mPurchaseListener = null; - } - - /** - * Disposes of object, releasing resources. If there is an in-progress async operation, this - * method will queue the dispose to occur after the operation has finished. - */ - public void disposeWhenFinished() { - synchronized (mAsyncInProgressLock) { - if (mAsyncInProgress) { - logDebug("Will dispose after async operation finishes."); - mDisposeAfterAsync = true; - } else { - try { - dispose(); - } catch (IabAsyncInProgressException e) { - // Should never be thrown, because we call dispose() only after checking that - // there's not already an async operation in progress. - } - } - } - } - - 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) - throws IabAsyncInProgressException { - launchPurchaseFlow(act, sku, requestCode, listener, ""); - } - - public void launchPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) - throws IabAsyncInProgressException { - launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData); - } - - public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener) throws IabAsyncInProgressException { - launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); - } - - public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) - throws IabAsyncInProgressException { - launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, 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 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 oldSkus A list of SKUs which the new SKU is replacing or null if there are none - * @param requestCode A request code (to differentiate from other responses -- as in - * {@link 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, List oldSkus, - int requestCode, OnIabPurchaseFinishedListener listener, String extraData) - throws IabAsyncInProgressException { - 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; - if (oldSkus == null || oldSkus.isEmpty()) { - // Purchasing a new item or subscription re-signup - buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, - extraData); - } else { - // Subscription upgrade/downgrade - if (!mSubscriptionUpdateSupported) { - IabResult r = new IabResult(IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE, - "Subscription updates are not available."); - flagEndAsync(); - if (listener != null) listener.onIabPurchaseFinished(r, null); - return; - } - buyIntentBundle = mService.getBuyIntentToReplaceSkus(5, mContext.getPackageName(), - oldSkus, 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(), - Integer.valueOf(0), Integer.valueOf(0), - Integer.valueOf(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 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; - } - - 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() throws IabException { - return queryInventory(false, null, 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 #queryInventoryAsync}. - * - * @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, moreSubsSkus); - 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 moreItemSkus as in {@link #queryInventory} - * @param moreSubsSkus as in {@link #queryInventory} - * @param listener The listener to notify when the refresh operation completes. - */ - public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus, - final List moreSubsSkus, final QueryInventoryFinishedListener listener) - throws IabAsyncInProgressException { - 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, moreItemSkus, moreSubsSkus); - } - 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) - throws IabAsyncInProgressException{ - queryInventoryAsync(false, null, 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) - throws IabAsyncInProgressException { - 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) - throws IabAsyncInProgressException { - 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).intValue(); - 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).intValue(); - 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) throws IabAsyncInProgressException { - synchronized (mAsyncInProgressLock) { - if (mAsyncInProgress) { - throw new IabAsyncInProgressException("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() { - synchronized (mAsyncInProgressLock) { - logDebug("Ending async operation: " + mAsyncOperation); - mAsyncOperation = ""; - mAsyncInProgress = false; - if (mDisposeAfterAsync) { - try { - dispose(); - } catch (IabAsyncInProgressException e) { - // Should not be thrown, because we reset mAsyncInProgress immediately before - // calling dispose(). - } - } - } - } - - 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; - } - - // Split the sku list in blocks of no more than 20 elements. - ArrayList> packs = new ArrayList>(); - ArrayList tempList; - int n = skuList.size() / 20; - int mod = skuList.size() % 20; - for (int i = 0; i < n; i++) { - tempList = new ArrayList(); - for (String s : skuList.subList(i * 20, i * 20 + 20)) { - tempList.add(s); - } - packs.add(tempList); - } - if (mod != 0) { - tempList = new ArrayList(); - for (String s : skuList.subList(n * 20, n * 20 + mod)) { - tempList.add(s); - } - packs.add(tempList); - } - - for (ArrayList skuPartList : packs) { - Bundle querySkus = new Bundle(); - querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList); - 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) - throws IabAsyncInProgressException { - 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. - */ - 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) - */ - 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. - */ - 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. - */ - 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. - */ - void onConsumeMultiFinished(List purchases, List results); - } - - /** - * Exception thrown when the requested operation cannot be started because an async operation - * is still in progress. - */ - public static class IabAsyncInProgressException extends Exception { - public IabAsyncInProgressException(String message) { - super(message); - } - } -} +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +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 com.android.vending.billing.IInAppBillingService; + +import org.json.JSONException; + +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. It is invalid to + * dispose the object while an asynchronous operation is in progress. You can + * call {@link #disposeWhenFinished()} to ensure that any in-progress operation + * completes before the object is disposed. + * + * 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. + * + */ +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_SERVICE_UNAVAILABLE = 2; + 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; + public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011; + // 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"; + // Ensure atomic access to mAsyncInProgress and mDisposeAfterAsync. + private final Object mAsyncInProgressLock = new Object(); + // 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; + // Do we need to dispose this object after an in-progress asynchronous operation? + boolean mDisposeAfterAsync = false; + // Are subscriptions supported? + boolean mSubscriptionsSupported = false; + // Is subscription update supported? + boolean mSubscriptionUpdateSupported = 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; + mSubscriptionUpdateSupported = false; + return; + } else { + logDebug("In-app billing version 3 supported for " + packageName); + } + + // Check for v5 subscriptions support. This is needed for + // getBuyIntentToReplaceSku which allows for subscription update + response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS); + if (response == BILLING_RESPONSE_RESULT_OK) { + logDebug("Subscription re-signup AVAILABLE."); + mSubscriptionUpdateSupported = true; + } else { + logDebug("Subscription re-signup not available."); + mSubscriptionUpdateSupported = false; + } + + if (mSubscriptionUpdateSupported) { + mSubscriptionsSupported = true; + } else { + // 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); + mSubscriptionsSupported = false; + mSubscriptionUpdateSupported = false; + } + } + + 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 intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0); + if (intentServices != null && !intentServices.isEmpty()) { + // service available to handle that Intent + 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() throws IabAsyncInProgressException { + synchronized (mAsyncInProgressLock) { + if (mAsyncInProgress) { + throw new IabAsyncInProgressException("Can't dispose because an async operation " + + "(" + mAsyncOperation + ") is in progress."); + } + } + logDebug("Disposing."); + mSetupDone = false; + if (mServiceConn != null) { + logDebug("Unbinding from service."); + if (mContext != null) mContext.unbindService(mServiceConn); + } + mDisposed = true; + mContext = null; + mServiceConn = null; + mService = null; + mPurchaseListener = null; + } + + /** + * Disposes of object, releasing resources. If there is an in-progress async operation, this + * method will queue the dispose to occur after the operation has finished. + */ + public void disposeWhenFinished() { + synchronized (mAsyncInProgressLock) { + if (mAsyncInProgress) { + logDebug("Will dispose after async operation finishes."); + mDisposeAfterAsync = true; + } else { + try { + dispose(); + } catch (IabAsyncInProgressException e) { + // Should never be thrown, because we call dispose() only after checking that + // there's not already an async operation in progress. + } + } + } + } + + 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) + throws IabAsyncInProgressException { + launchPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) + throws IabAsyncInProgressException { + launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener) throws IabAsyncInProgressException { + launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) + throws IabAsyncInProgressException { + launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, 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 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 oldSkus A list of SKUs which the new SKU is replacing or null if there are none + * @param requestCode A request code (to differentiate from other responses -- as in + * {@link 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, List oldSkus, + int requestCode, OnIabPurchaseFinishedListener listener, String extraData) + throws IabAsyncInProgressException { + 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; + if (oldSkus == null || oldSkus.isEmpty()) { + // Purchasing a new item or subscription re-signup + buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, + extraData); + } else { + // Subscription upgrade/downgrade + if (!mSubscriptionUpdateSupported) { + IabResult r = new IabResult(IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE, + "Subscription updates are not available."); + flagEndAsync(); + if (listener != null) listener.onIabPurchaseFinished(r, null); + return; + } + buyIntentBundle = mService.getBuyIntentToReplaceSkus(5, mContext.getPackageName(), + oldSkus, 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(), + Integer.valueOf(0), Integer.valueOf(0), + Integer.valueOf(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 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; + } + + 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() throws IabException { + return queryInventory(false, null, 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 #queryInventoryAsync}. + * + * @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, moreSubsSkus); + 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 moreItemSkus as in {@link #queryInventory} + * @param moreSubsSkus as in {@link #queryInventory} + * @param listener The listener to notify when the refresh operation completes. + */ + public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus, + final List moreSubsSkus, final QueryInventoryFinishedListener listener) + throws IabAsyncInProgressException { + 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, moreItemSkus, moreSubsSkus); + } + 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) + throws IabAsyncInProgressException{ + queryInventoryAsync(false, null, 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) + throws IabAsyncInProgressException { + 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) + throws IabAsyncInProgressException { + 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).intValue(); + 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).intValue(); + 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) throws IabAsyncInProgressException { + synchronized (mAsyncInProgressLock) { + if (mAsyncInProgress) { + throw new IabAsyncInProgressException("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() { + synchronized (mAsyncInProgressLock) { + logDebug("Ending async operation: " + mAsyncOperation); + mAsyncOperation = ""; + mAsyncInProgress = false; + if (mDisposeAfterAsync) { + try { + dispose(); + } catch (IabAsyncInProgressException e) { + // Should not be thrown, because we reset mAsyncInProgress immediately before + // calling dispose(). + } + } + } + } + + 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; + } + + // Split the sku list in blocks of no more than 20 elements. + ArrayList> packs = new ArrayList>(); + ArrayList tempList; + int n = skuList.size() / 20; + int mod = skuList.size() % 20; + for (int i = 0; i < n; i++) { + tempList = new ArrayList(); + for (String s : skuList.subList(i * 20, i * 20 + 20)) { + tempList.add(s); + } + packs.add(tempList); + } + if (mod != 0) { + tempList = new ArrayList(); + for (String s : skuList.subList(n * 20, n * 20 + mod)) { + tempList.add(s); + } + packs.add(tempList); + } + + for (ArrayList skuPartList : packs) { + Bundle querySkus = new Bundle(); + querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList); + 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) + throws IabAsyncInProgressException { + 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. + */ + 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) + */ + 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. + */ + 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. + */ + 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. + */ + void onConsumeMultiFinished(List purchases, List results); + } + + /** + * Exception thrown when the requested operation cannot be started because an async operation + * is still in progress. + */ + public static class IabAsyncInProgressException extends Exception { + public IabAsyncInProgressException(String message) { + super(message); + } + } +} diff --git a/qpython/src/main/java/org/qpython/qpy/utils/iap/IabResult.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/IabResult.java index 3d149f207f87872c8337e9b19d3f3c34189fc608..028cd2c305de5d45940cc83d1a84fd49b72ed056 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/IabResult.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/IabResult.java @@ -1,45 +1,45 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -/** - * 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(); } -} - +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +/** + * 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/qpython/src/main/java/org/qpython/qpy/utils/iap/Inventory.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/Inventory.java index 456b600f62f21b0038083fc4083df7519e8f3d9a..d5dd6505ef6b3f4ba643c405a5715fb242584450 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/Inventory.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/Inventory.java @@ -1,91 +1,91 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -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); - } -} +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +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/qpython/src/main/java/org/qpython/qpy/utils/iap/Purchase.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/Purchase.java index 5eb9c8220addda0301958332eb280eecb1301260..cfa95959700d2ee93ec812931466f7550db16a16 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/Purchase.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/Purchase.java @@ -1,66 +1,66 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -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; - boolean mIsAutoRenewing; - - 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")); - mIsAutoRenewing = o.optBoolean("autoRenewing"); - 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; } - public boolean isAutoRenewing() { return mIsAutoRenewing; } - - @Override - public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; } -} +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +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; + boolean mIsAutoRenewing; + + 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")); + mIsAutoRenewing = o.optBoolean("autoRenewing"); + 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; } + public boolean isAutoRenewing() { return mIsAutoRenewing; } + + @Override + public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; } +} diff --git a/qpython/src/main/java/org/qpython/qpy/utils/iap/Security.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/Security.java index bff24e9c9c3e3bb89ab069d96624ba2bf4f0c0ca..973a98949fba842952ee6684333278fab4bdccce 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/Security.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/Security.java @@ -1,121 +1,121 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -import android.text.TextUtils; -import android.util.Base64; -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, Base64.DEFAULT); - 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); - } - } - - /** - * 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) { - byte[] signatureBytes; - try { - signatureBytes = Base64.decode(signature, Base64.DEFAULT); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Base64 decoding failed."); - return false; - } - try { - Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); - sig.initVerify(publicKey); - sig.update(signedData.getBytes()); - if (!sig.verify(signatureBytes)) { - 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."); - } - return false; - } -} +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +import android.text.TextUtils; +import android.util.Base64; +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, Base64.DEFAULT); + 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); + } + } + + /** + * 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) { + byte[] signatureBytes; + try { + signatureBytes = Base64.decode(signature, Base64.DEFAULT); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Base64 decoding failed."); + return false; + } + try { + Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); + sig.update(signedData.getBytes()); + if (!sig.verify(signatureBytes)) { + 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."); + } + return false; + } +} diff --git a/qpython/src/main/java/org/qpython/qpy/utils/iap/SkuDetails.java b/qpython/src/main/java/org/qpython/qpy/utils/iap/SkuDetails.java index b48817e8e6bdf01aa7d4ad810e94c93517aa61d8..2c92938a3d6a917da4e212edc7ca7ef5a318b4f7 100644 --- a/qpython/src/main/java/org/qpython/qpy/utils/iap/SkuDetails.java +++ b/qpython/src/main/java/org/qpython/qpy/utils/iap/SkuDetails.java @@ -1,64 +1,64 @@ -/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Represents an in-app product's listing details. - */ -public class SkuDetails { - private final String mItemType; - private final String mSku; - private final String mType; - private final String mPrice; - private final long mPriceAmountMicros; - private final String mPriceCurrencyCode; - private final String mTitle; - private final String mDescription; - private final 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"); - mPriceAmountMicros = o.optLong("price_amount_micros"); - mPriceCurrencyCode = o.optString("price_currency_code"); - mTitle = o.optString("title"); - mDescription = o.optString("description"); - } - - public String getSku() { return mSku; } - public String getType() { return mType; } - public String getPrice() { return mPrice; } - public long getPriceAmountMicros() { return mPriceAmountMicros; } - public String getPriceCurrencyCode() { return mPriceCurrencyCode; } - public String getTitle() { return mTitle; } - public String getDescription() { return mDescription; } - - @Override - public String toString() { - return "SkuDetails:" + mJson; - } -} +/* Copyright (c) 2012 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 org.qpython.qpy.utils.iap; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an in-app product's listing details. + */ +public class SkuDetails { + private final String mItemType; + private final String mSku; + private final String mType; + private final String mPrice; + private final long mPriceAmountMicros; + private final String mPriceCurrencyCode; + private final String mTitle; + private final String mDescription; + private final 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"); + mPriceAmountMicros = o.optLong("price_amount_micros"); + mPriceCurrencyCode = o.optString("price_currency_code"); + mTitle = o.optString("title"); + mDescription = o.optString("description"); + } + + public String getSku() { return mSku; } + public String getType() { return mType; } + public String getPrice() { return mPrice; } + public long getPriceAmountMicros() { return mPriceAmountMicros; } + public String getPriceCurrencyCode() { return mPriceCurrencyCode; } + public String getTitle() { return mTitle; } + public String getDescription() { return mDescription; } + + @Override + public String toString() { + return "SkuDetails:" + mJson; + } +} diff --git a/qpython/src/ol/res/layout/activity_local.xml b/qpython/src/main/res/layout/activity_local.xml similarity index 100% rename from qpython/src/ol/res/layout/activity_local.xml rename to qpython/src/main/res/layout/activity_local.xml diff --git a/qpython/src/main/res/values-ja/arrays.xml b/qpython/src/main/res/values-ja/arrays.xml deleted file mode 100644 index f917ed879a83fa86961275b0b5b688162261e93e..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-ja/arrays.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - ステータスバーを表示 - ステータスバーを隠す - - - - - アクションバーを表示 - アクションバーを隠す(画面の上部かメニューボタンを押すと再表示) - - - - 自動 - 水平 - 垂直 - - - - 4 x 8px - 6 pt - 7 pt - 8 pt - 9 pt - 10 pt - 12 pt - 14 pt - 16 pt - 20 pt - 24 pt - 28 pt - 32 pt - 36 pt - 42 pt - 48 pt - 64 pt - 72 pt - 96 pt - 144 pt - 288 pt - - - - 白の背景・黒の文字 - 黒の背景・白の文字 - 青の背景・白の文字 - 黒の背景・緑の文字 - 黒の背景・黄の文字 - 黒の背景・赤の文字 - ホログラム - 高露光 - 低露光 - Linuxスタイル - - - - すべてのウィンドウを閉じる - 現在のウィンドウを閉じる - セッションを維持したまま現在のウィンドウを閉じる - ESCをコンソールに送る - TABをコンソールに送る - - - - Ball - \@ - 左Alt - 右Alt - 音量上げボタン - 音量下げボタン - カメラボタン - 無し - - - - Ball - \@ - 左Alt - 右Alt - 音量上げボタン - 音量下げボタン - カメラボタン - 無し - - - - 文字 - 単語 - - - - - - Ball - \@ - 左Alt - 右Alt - 音量上げボタン - 音量下げボタン - カメラボタン - 無し - - - - - Ball - \@ - 左Alt - 右Alt - 音量上げボタン - 音量下げボタン - カメラボタン - 無し - - - - - - - - クラシック - ダーク - マトリクス - スカイ - ドラキュラ - - - \ No newline at end of file diff --git a/qpython/src/main/res/values-ja/strings.xml b/qpython/src/main/res/values-ja/strings.xml deleted file mode 100644 index a00741f45c99b4fc8a88335e3c3c8a27743739df..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-ja/strings.xml +++ /dev/null @@ -1,494 +0,0 @@ - - - コンソール - 最近 - スニペット - スクリプト - プロジェクト - - 設定 - エディタ - コミュニティ - - さらに - SL4Aサーバー - FTPサーバー - - - - コンソール - 設定 - 新しくコンソールを開始 - コンソールを閉じる - 現在のコンソール一覧 - コンソールをリセット - メールを送る... - 特殊キー - 仮想キーボードを表示・非表示 - - 現在の状態はリセットされます。 - - wakelockを有効化 - wakelockを無効化 - WIFI lockを有効化 - WIFI lockを無効化 - - テキストの編集 - テキストを選択 - すべてコピー - Ctrlキーを送信 - Fnキーを送信 - - コンソール%1$d - - セッションは終了しました。 - - - 画面 - - ステータスバー - ステータスバーの表示・非表示 - ステータスバー - - アクションバー - アクションバーの動作を選択(Android 3.0以上) - アクションバーの動作 - - 画面の向き - 画面の向きの動作を選択 - 画面の向きの動作 - - テキスト - - デフォルトでUTF-8を使用 - デフォルトでUTF-8を使用中 - - 文字の大きさ - 文字サイズを選択 - 文字の大きさ - - 文字色 - 文字の色を選択 - 文字色 - - キーボード - - 戻るボタンの動作 - 戻るボタンの動作を選択 - 戻るボタンの動作 - - Ctrlキー - Ctrlキーの動作を選択 - Ctrlキー - - Fnキー - Fnキーの動作を選択 - Fnキー - - 入力方式 - 入力方式を選択 - 入力方式 - - シェル - コンソール - シェルを選択 - シェル - - 自動実行コマンド - スタート時に自動実行するコマンドを設定 - 自動実行コマンド - - コンソールのタイプ - コンソールのタイプを設定 - コンソールのタイプ - - 終了時にコンソールを閉じる - 終了時にコンソールを閉じる - - PATHを検証 - 不正なPATHを削除 - - PATHの拡張を許可 - PATHへのコマンドの追加を許可 - - PATHの先頭に追加することを許可 - PATH既存のコマンドの上書を許可 - - HOMEディレクトリ - HOMEディレクトリとして使用する書き込み可能なディレクトリ - - CtrlとFnキー - CTRL+Space : Control-@ (NUL)\nCTRL+A..Z : Control-A..Z\nCTRL+5 : Control-]\nCTRL+6 : Control-^\nCTRL+7 : Control-_\nCTRL+9 : F11\nCTRL+0 : F12 - Ctrl キー unset - FN+1..9 : F1-F9\nFN+0 : F10\nFN+W : Up\nFN+A : Left\nFN+S : Down\nFN+D : Right\nFN+P : PageUp\nFN+N : PageDown\nFN+T : Tab\nFN+L : | (pipe)\nFN+U : _ (アンダースコア)\nFN+E : Control-[ (ESC)\nFN+X : Delete\nFN+I : Insert\nFN+H : Home\nFN+F : End\nFN+. : Control-\\\n - Fnキーの設定を削除 - - コンソールを閉じますか? - - コンソールのコピー - QPythonのフィードバック - フィードバック... - メーラーアプリが見つかりませんでした。 - - AltキーをESCとして使う - AltキーをESCとして使用中 - AltキーのESCとしての使用をやめる - - マウスイベントの送信 - タッチとスクロールを無視しますか? - - スクリプト - プロジェクト - - - キーボードショートカットを使用 - Ctrl-Tab: コンソールの切り替え、 Ctrl-Shift-N: 新規コンソール、 Ctrl-Shift-V: 貼り付け - キーボードショートカットが無効になっています - ヘルプ - 活動期間 - - - "Term shortcut" - Find command - command - --example=\""a\" - SELECT SHORTCUT TARGET - Command terminal requires full mPath, no arguments. For other commands use Arguments terminal (ex: cd /sdcard). - Add to dictionary - Shortcut label\: - Text icon - MAKE TEXT ICON - - Term shortcut - - FILE SELECTOR - External storage not available - Or enter mPath here. - Change theme - - LOCK - Enter icon text - - https://www.qpython.org - - 読み取り専用 - 探索 - はい - いいえ - 了解 - 閉じる - 変更なし - フィードバック - QPythonについて - ファイルが見つかりません - rootモードを有効にする - 送信 - 有効なカメラが見つかりませんでした。カメラへのアクセス権限を確認してください。 - ストレージの読み取り権限がありません。 - - - python - QPY - ローカルアプリ - タスク - rootモード - 評価する - 無効化 - https://pypi.python.org/simple/ - add - SL4Aは動作中です - SL4Aは動作していません。 - Server taggle - 警告 - この操作を続行すると、設定やダウンロードしたライブラリがすべて失われます。続行しますか? - ライブラリ - ロケーション - - - - 名称未設定 - %1$s - %1$s * - [%1$s] - - 設定 - - - 表示 - 行番号を表示 - 文字を折り返す - フォントの変更 - 文字の大きさ - 色のテーマ - スクロースの高速化のために整理する - ページの左側に行番号を表示 - 長すぎる行を折り返す - パフォーマンスを検証しない - - - - - "保存しますか? 保存していない内容は失われます。" - 保存 - 削除 - 保存しない - 取り消し - ファイル名 - 検索 - 履歴 - 読み取り専用で開く - QEditはAndroid用のテキストエディタです。Python / HTMLの編集に特化しており、使いやすいエディタです。 - - - "QEditは素晴らしいテキストエディタです。" - - ファイルが既に存在します。別の名前にしてください。 - - 動画 {0} - 再生 - このページに動画はありません。 - A8 Video Playerを先にインストールしてください。 - ダウンロードして保存 - QBrowser - - - 共有 - スニペットとして保存 - すでにスニペットが存在します。別の名前で試してください。 - スニペットが次の名前で保存されました: - スニペットの挿入に失敗しました - 有効な数字を入力してください。 - 行を自動ハイライト - %s行より少なければ自動ハイライト - QEditは以下のタイプのファイルのみプレビュー・実行をサポートします。\n.py, .sh, .lua, .html, .md. - - Luaを実行するにはQLuaを先にインストールする必要があります。 - Pythonを実行するにはQPythonを先にインストールする必要があります。 - スニペット - - OK - 連絡を取る - ライセンス - support@qpython.org - メールを開く... - "https://market.android.com/developer?pub=Xavier+Gouchet" - "https://play.google.com/store/apps/details?id=%s" - "ほかのアプリ" - "このアプリを評価する \n フィードバックを残す" - "どうぞご遠慮なくフィードバック、機能提案、または不具合報告をしてください。. " - "このソフトウエアは現状のまま提供されており、いかなる保証もありません。このソフトウエアの著作者は、このソフトウエアを利用したことによって発生したいかなる損害でもその責任を負いません。" - Artistic Licence - "このアプリに使用されているアイコンはQPythonの著作者の製作であり、その権利はその著作者に帰属します。" - "最新情報" - 保存 - ファイル名を指定して保存 - 開く - ファイル名 - そのファイルは既に存在します。名前を変えてください。 - 空ファイル - 確認 - 取り消し - 新規プロジェクト - エラーが発生しました。 - プロジェクト名 - 移動する - フォントファイルを選択(.ttf) - エクスプローラ - 新規スクリプト - スクリプト名を入力 - - 貼り付け - ダウンロード失敗 - reset_storage - running_state - username - password - portNum - show_password - chrootDir - stayAwake - about - 不正なディレクトリです - すでに最新です - QPythonはAndroidデバイス上で動作するスクリプトエンジンです。Pythonインタプリタ、コンソール、エディタ、SL4Aライブラリを組み込みで持っており、それらはAndroid上でのPythonの実行を可能にします。https://www.qpython.com/privacy.html - 新規フォルダ - フォルダ名 - 名称未設定フォルダ - 操作に失敗しました。別の名前で再度お試しください。 - ファイルが存在しないため、ファイルを開けません。 - 先に保存しますか? - 必要ありません。 - 変換に失敗しました。 - このファイルを消去しますか? - 名前の変更 - 名前を変更できませんでした。 - 削除できませんでした。 - 実行時ログ: %s - [フィードバック]\n\n - 実行時ログ - - [ %s ] - このデバイスはルート化されていません。 - APKをダウンロードしますか? - WiFi未接続の場合、モバイル通信料がかかる可能性があります。 - 新規パッケージをダウンロード - アップデートの確認 - バックグラウンドで実行する - スキャン - QRCodeからコードを読み込み - - コース - 接続失敗 - コース - プログラム - OpenGL2はサポートされていません。 - Pythonコードを抽出できません。 - 設定を読み込めませんでした。 - コンソールをバックグラウンドで続行しますか? - ありがとうございます! - PYPIを設定 - すでに最新です。現在のバージョンは - 報酬 - しまった! \nインターネット接続に問題があるようです! - Google Playに接続できませんでした。ネットワークを確認してください。 - 応援してくださってありがとうございます! - ステータスバーの表示・非表示はコンソールを再起動すると適用されます。 - 最新の状態に更新する - リセットに成功しました。 - この操作はアプリの再起動後に適用されます。 - アプリを再起動 - コンソールを続行できません。 - TermServiceに接続できませんでした。! - QPythonコンソール動作中 - QPythonコンソール - 推奨 - 最新 - モバイル上の最高のPythonアプリケーション - Googleでサインイン - プライバシーポリシーに同意する必要があります。]]> - 実行のため、エディタにコピーします - スクリプト - 共有済み - Gistのタイトル - 説明 - 値が長すぎます。140文字以内にしてください。 - スニペットの名前を入力 - ようこそ、%s]]>さん - Re @%1$s]]>:\u0020 %2$s - @%1$s]]>:\u0020 %2$s - サインイン - 認証に失敗しました。 - サインアウト - Me - ストレージへのアクセスを許可をする必要があります。 - Googleのサーバーに接続できませんでした。ネットワークを確認してください。 - 正常にコードをアップロードしました。 - コードをアップロードできませんでした。 - プログラム - まだ何もありません。 :( - 初めにコメントしますか? - コメントに内容がありません。 - 初期 - 共有前に保存する必要があります。 - サインインしてください。 - サインインせずにコードをアップロードすることはできません。 - ログインする - クラウド上にコードがありません。 - ディレクトリの作成に失敗しました。 - このファイルタイプのアップロードはサポートされていません。 - - オンラインのファイル:
- サイズ: %4$s
- 最終更新: %3$s

- ローカルのファイル:
- 大きさ: %2$s
- 最終更新: %1$s

- 上書きしますか?]]>
- 上書きできませんでした。時間をおいて再度お試しください。 - オンラインのファイルを正常に削除しました。 - ファイルを正常に削除しました。 - リクエストに失敗しました。画面をタップして再試行します。 - ログアウトしますか? - ログ通知 - アプリの通知 - Notification - ローカルのファイルを上書きしますか? - ファイルがダウンロードされました。 - ログイン中にエラーが発生しました。 - 容量 (%d / 100文件) - 空き容量 (? / 10文件) - - ショートカット%sは正常に作成されました。 - これらの権限を許可する必要があります。 - 容量が足りません。 - このフォルダにはファイルが存在しません。 - 最近ファイルを開いていません。 - このファイルは開けません。 - プロジェクトの構造 - エディタ - まだサポートされていません。 - 新規ファイル - ツール - 依存しているパッケージをインストールしてからAIPYをインストールしてください。]]> - Welcome to Use QPython - - - - 更新: %s - AIPyが見つかりません。AIPyを探すか、以下の手順でインストールしてください。\n - 1. AIPyアプリをインストール\n - 2. QPythonにファイル書き込みを許可\n - 3. 右上部の角にあるボタンを押して最新の状態に更新\n\n - 動かない場合は遠慮なくメールかGithubに連絡してください。 - - このパッケージは既にインストールされています。 - インストール成功 - インストールに失敗しました。時間をおいて再度お試しください。 - パッケージのインストールに失敗しました。時間をおいて再度お試しください。 - AIPyのアプリからインストールとセットアップをしてください。 - AIPyはQPY3をまだサポートしていません。 - - アルバム - 最終更新: %s - データサーバー - ローカルのキャッシュを使う - 毎回再読み込みする - ライブラリ - 実行 - - if :\n - def ():\n - for\u0020\u0020in :\n - import\u0020\n - from\u0020\u0020import\u0020\n - class\u0020\u0020: - elif\u0020\u0020:\n - else:\n - return\n - - ファイル - Python 3をインストール - 現在Python 2を使用中 - 現在Python 3を使用中 - Python 3に切り替え - 履歴 - QPythonの特集 - 履歴はありません - Googleサービスなしでログインできません。 - ネットワークなしでpipを開くことはできません。 - QPY3はインストールされていません。 - アプリマーケットに移動 - パッケージが解放されていません。 - 先にPython3からパッケージを開放してください。 - Python3を開く - zipの解凍に失敗しました。 - 再試行してください。 - ブラウザ - 現在Python2を使用中です。Python3に切り替えますか? - 設定に移動 - ネットワークにラグが発生しています。しばらくお待ちください。 - You can run QPython script even it\'s in background if this mode is enabled. - Disable - QPython will restart to apply changes. - "Please grant the creat shortcut permission. " -
diff --git a/qpython/src/main/res/values-ja/toasts.xml b/qpython/src/main/res/values-ja/toasts.xml deleted file mode 100644 index 440eb69a7a61bce42d86ccab77186186858ae4c3..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-ja/toasts.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - "ファイルが見つかりませんでした。最近使ったファイルの項目から削除します。" - 最近使ったファイルの項目から削除しました。 - ファイルが開けません。URIが不正です。 - ファイルが開けません。 - ファイルブラウザを読み込めません。 - メモリ不足のため、ファイルが開けません。 - ファイルが開けません。 (ファイルが大きすぎるか、破損しています) - パスがnullのため、保存できません。 - 一時ファイルに保存することはできません。 - 保存できません (一時ファイルのコピーでエラーが発生しました。) - 保存できません (一時ファイルのコピーでエラーが発生しました。) - 保存しました。 - "このフォルダは書き込み可能ではないため使用できません。" - "このファイルは読み取り可能ではないため使用できません。" - ファイルが見つかりませんでした。 - これ以上ファイルが見つかりませんでした。 - 検索バーに入力すると検索できます。 - ファイル名を空にすることはできません。 - これ以上元に戻せません。 - 既定のファイルが見つかりません。設定から新たに選択してください。 - "既定のファイルが読み込み可能ではないため使用できません" - 新しいウィジェットに関連付けるファイルを選択してください。 - ファイルを選択して開きます。 - ホーム画面からTedを起動したときに開くファイルを選択してください。 - "現在開いているファイルは自動保存されました。次回起動時はこのファイルが自動で開かれます。" - - - - "フォルダが存在していません。" - フォルダではありません。 - "読み取り可能なフォルダではありません。" - このデバイス上にPlay Storeが存在しません。 - - \ No newline at end of file diff --git a/qpython/src/main/res/values-ru/arrays.xml b/qpython/src/main/res/values-ru/arrays.xml deleted file mode 100644 index 28ea4ea9889fdaad81846572f3a2b2be12683d4e..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-ru/arrays.xml +++ /dev/null @@ -1,191 +0,0 @@ - - - - - Показать Status bar - Скрыть Status bar - - - - - Показать Панель действий - Скрыть Панель действий(Нажмите на верхнюю часть экрана или кнопку меню, чтобы показать снова) - - - - Автоматически - Горизонтальная - Вертикальная - - - - 4 x 8 Pixel - 6 point type - 7 point type - 8 point type - 9 point type - 10 point type - 12 point type - 14 point type - 16 point type - 20 point type - 24 point type - 28 point type - 32 point type - 36 point type - 42 point type - 48 point type - 64 point type - 72 point type - 96 point type - 144 point type - 288 point type - - - - Чёрные символы на белом фоне - Белые символы на чёрном фоне - Белые символы на голубом фоне - Зелёные символы на чёрном фоне - Жёлтые символы на чёрном фоне - Красные символы на чёрном фоне - Стиль Holo - over-exposed - lack-exposed - Стиль Linux - - - - Закрыть все окна - Закрыть текущее окно - Закрыть текущее окно, но оставить сессии работать - Отправить ESC в терминал - Отправить TAB в терминал - - - - Ball - \@ - Left-Alt - Right-Alt - Vol-Up - Vol-Dn - Camera - None - - - - Ball - \@ - Left-Alt - Right-Alt - Vol-Up - Vol-Dn - Camera - None - - - - Символьный - Слова - - - - Открыть оедактором - Открыть в ... - Сканировать заново - - - - - - Ball - \@ - Left-Alt - Right-Alt - Vol-Up - Vol-Dn - Camera - None - - - - - Ball - \@ - Left-Alt - Right-Alt - Vol-Up - Vol-Dn - Camera - None - - - - auto - landscape - portrait - - - - Скрипт - Проект - - - - - - Пустой файл - Скрипт - Веб-приложение (Проект) - Консольное приложение (Проект) - Приложение Kivy (Проект) - - - - - Классическая - Негатив - Матрица - Небесная - Дракула - - - 0 - 1 - 2 - 3 - 4 - - - - - - - - - - - - 100 - 300 - 600 - 1000 - 2000 - - - diff --git a/qpython/src/main/res/values-ru/strings.xml b/qpython/src/main/res/values-ru/strings.xml deleted file mode 100644 index fef6b8bddfb9be656d3f36dc048d19baa3a0ba3b..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-ru/strings.xml +++ /dev/null @@ -1,437 +0,0 @@ - - Терминал - Библиотека - Недавние - Скрипт - Проект - - Настройки - Редактор - Сообщество - - Больше - SL4A Сервер - FTP сервер - - - Терминал - Настройки - Открыть новое терминал - Закрыть терминал - Список окон - Перезапустить терминал - Отправить email на… - Особые ключи - Открыть/Закрыть клавиатуру - - The state at present will be reset. - - Включить блокировку сна - Отключить блокировку сна - Включить блокировку WIFI - Отключить блокировку WIFI - - Редактировать текст - Выбрать текст - Копировать всё - Нажать клавишу control - Нажать клавишу Fn - - Окно %1$d - - Сессия не активна - - - Экран - - Статусбар - Показать/Скрыть статусбар - Статусбар - - Action Bar - Выберите поведение панели действий(Android 3.0 и выше) - Поведение панели действий - - Ориентация экрана - Выберите ориентацию экрана - ориентация экрана - - Текст - - Использовать UTF-8 по умолчанию - Использовать UTF-8 по умолчанию - - Размер шрифта - Выберите размер шрифта - Размер шрифта - - Цвет текста - Выберите цвет текста - Цвет текста - - Клавиатура - - Поведение кнопки назад - Выберите поведение кнопки назад - Поведение кнопки назад - - Клавиша Ctrl - Выберите поведение клавиши Ctrl - Клавиша Ctrl - - Клавиша Fn - Выберите поведение клавиши Fn - Клавиша Fn - - Способ ввода - Выберите способ ввода - Способ ввода - - Оболочка - Терминал - Выберите оболочку - Оболочка - - Команда инициализации - Автоматическое выполнение команды инициализации при запуске - Команда инициализации - - Тип терминала - Установить оболочку терминала - Тип терминала - - Закрывать терминал при выходе - Закрывать терминал при выходе - - проверить PATH - Очистить неправильный PATH - - Разрешить расширение PATH - Разрешать добавлять команды к PATH - - Разрешить препозицию PATH - Разрешить перезапись существующих команд в PATH - - Домашняя папка - Использовать как переменную пути PATH - - Клавиши Ctrl и Fn - CTRLKEY Пробел : Control-@ (NUL)\nCTRLKEY A..Z : Control-A..Z\nCTRLKEY 5 : Control-]\nCTRLKEY 6 : Control-^\nCTRLKEY 7 : Control-_\nCTRLKEY 9 : F11\nCTRLKEY 0 : F12 - Разбиндить клавишу Ctrl - FNKEY 1..9 : F1-F9\nFNKEY 0 : F10\nFNKEY W : Up\nFNKEY A : Left\nFNKEY S : Down\nFNKEY D : Right\nFNKEY P : PageUp\nFNKEY N : PageDown\nFNKEY T : Tab\nFNKEY L : | (столбик)\nFNKEY U : _ (нижнее подчёркивание)\nFNKEY E : Control-[ (ESC)\nFNKEY X : Delete\nFNKEY I : Insert\nFNKEY H : Home\nFNKEY F : End\nFNKEY . : Control-\\\n - Fn key unset - - Закрыть терминал? - - Копия терминала - Обратная связь - QPython - Обратная связь ... - Не удалось найти подходящее приложение для отправки E-mail - - Использовать Alt как ESC - Используется Alt как ESC - Отменить использование Alt как ESC - - Отправить событие мыши - События касания и прокрутки игнорируются? - - скрипт - проект - - - Use Keyboard Shortcuts - Ctrl-Tab: пролистнуть терминал, Ctrl-Shift-N: новое терминал, Ctrl-Shift-V: вставить. - Сокращения для клавиатуры отключены. - Помощь - Называть здесь - - - "Временный ярлык" - Команда поиска - команда - --пример=\""a\" - ВЫБЕРИТЕ ЦЕЛЬ ДЛЯ ЯРЛЫКА - Command window requires full mPath, no arguments. For other commands use Arguments window (ex: cd /sdcard). - Добавить в директорию - Надпись ярлыка\: - Текстовая иконка - СДЕЛАТЬ ТЕКСТОВУЮ ИКОНКУ - - Временный ярлык - - ВЫБОР ФАЙЛА - Внешнее хранилище недоступно - Or enter mPath here. - Изменить тему - - БЛОКИРОВКА - Введите текст иконк - - https://www.qpython.org - - Новый файл - Только чтение - Файловый менеджер - Да - Нет - ОК - Закрыть - Нет изменений - Обратная свзяь - О приложении - Файл не существует - Включён - - - Отправить - Не удаётся подключиться к камере. Проверьте соответствующее разрешение для приложения. - Нет разрешения для доступа к хранилищу - - 0 найдено - %d текст заменён - - - English - Chinese - - - Использовать \\n(Linux/OSX) - Использовать \\r\\n(Windows) - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 100 - - - Автоматически - Горизонтальная - Вертикальная - - - - Открыть в редакторе - Открыть в ..... - Мканировать заново - - - - Net библиотека - - - - Net библиотека - Загружено - - - - python - QPY - Локальное приложение - Задача - Root режим - Оценить - Отключён - https://pypi.python.org/simple/ - добавить - SL4A сервер запущен - SL4A сеовер не активен - Переключить сервер - Предупреждение - Сброс удалит все данные и загруженные библиотеки. Вы уверены, что хотите продолжить? - Библиотека - Расположение - - - - - - QEdit - %1$s - QEdit - %1$s * - QEdit - [%1$s] - - QEdit - Настройки - - - Дисплей - Показывать номера строк - Перенос слов - Изменить шрифт - Размер текста - Цветовая схема - Бросьте для быстрой прокрутки - Отображать номера строк слева - Строки будут делиться, если они не умещаются на экран - Выключите, если у вас проблемы с производительностью - - - - - "Хотите сохранить файл? \nНесохранённые изменения будут потеряны." - Сохранить - Удалить - "Не сохранять" - Отмена - Имя файла - Поиск - История - Открыть только для чтения - QEdit это текстовый редактор для планшетов и телеыонов на платформе Android. Кроме редактирования теста, он может сделать разработку на python / HTML on гораздо более интересной и очень удобной. - - - "QEdit отличный редактор кода." - - Файл существует. Пожалуйста, измените название - - - Видео {0} - Запустить видео - На странице не найдены видео - Пожалуйста, установите A8 Video Player - Загрузить и сохранить как - QBrowser - - - Расшарить - Сохранить как фоагмент - Фрагменты существуют. Пожалуйста, измените название. - Фрагменты были сохранены как: - Не удалось вставить фрагмент - Пожалуйста, введите корректное число - Авто-подсветка строк - Авто-подсветка при количестве строк меньше, чем %s - QEdit поддерживает просмотр/запуск форматов\n.py, .sh, .lua, .html, .md. - - Пожалуйста, установите QLua для запуска скрипта - Пожалуйста, установите QPython для запуска скрипта - Snippets - - ОК - Контакты - Лицензия - support@qpython.org - Открыть почту, используя … - "https://market.android.com/developer?pub=Xavier+Gouchet" - "https://play.google.com/store/apps/details?id=%s" - "Взгляните на другие приложения от меня" - "Оценить приложение \n Оставить отзыв" - "Вы можете связаться со мной для обсуждения любого бага или новой фнкции. " - "Это программное обеспечение предоставляется 'как есть', без каких-либо гарантий явно выраженных или подразумеваемых. Ни при каких обстоятельствах автор не несет ответственности за любой ущерб, возникший в результате использования этого программного обеспечения." - Художественная лицензия - "Все иконки для этого приложения были созданы и принадлежат его автору: QPython." - "Что нового" - Сохранить - Сохранить как - Открыть - Имя файла - Файл сушествует. Пожалуйста, переименуйте его. - Пустой файл -w Подтвердить - Отмена - Новый проект - Возникла ошибка. - Имя проекта - Переместиться к - Выберите файл шрифта(.ttf) - Файловый менеджер - Новый скрипт - Ваедите имя скрипта - - Вставить - Не удалось загрузить - reset_storage - running_state - username - password - portNum - show_password - chrootDir - stayAwake - about - Недоступная директория - Уже последняя - QPython это движок скриптов, работающий на устройствах Android, таких как телефон или планшет. Он включает в себя интерпретатор Python, консоль, редактор, библиотеку SL4A для Android, что позволяет вашему устройству Android работать с Python\'ом https://www.qpython.com/privacy.html - Новая папка - Название папки - untitled folder - Операция провалена! Пожалуйста, выберите другое имя папки - Не удалось открыть файл. Файл не существует - Сохранить изменения? - Нет, и так сойдёт - Не удалось конвертировать - Вы хотите удалить этот файл? - Переименовать - Не удалось переименовать - Не удалось удалить - Лог: %s - Лог - - [Обоатная связь]\n\n - [ %s ] - Устройство ещё не рутировано - Загрузить APK? - Загрузка может быть платной, если вы не используете WIFI - Загрузка нового пакета - Проверка обновлений - Запустить в фоновом режиме - Сканировать - Чтение кода с QRCode - - Курсы - Потеряно соединение - Курсы - Программы - OpenGL2 не поддерживается - Невозможно извлечь файлы python - Не удалось получить данные ностроек - Оставить терминал работать в фоновом режиме? - СПАСИБО! - Установить PYPI - Установлена последняя версия. Номер версии: - Reward - Ой! \n Похоже, у вас пробелмы с сетью… - Не удаётся подключиться к сервисам Google Play. Пожалуйста, проверьте ваше соединение. - Спасибо за вашу помощь! - Изменение праметра видимости статус-бара будет применено только при следующем запуске терминала. - Обновить - Успешный перезапуск - Эта операция будет актовной только после перезапуска - Перезапуск - Не удалось восстановить консоль - Не удалось забиндить TermService! - Консоль QPython запущена - Консоль QPython - Рекомендованное - Последнее - Лучшее приложение Python на мобильных устройствах - Войти через Google - - Скопируйте в редактор, чтобы запустить - Мои скрипты - Моё расшаренное - Фрагмент - Gist заголовок - Описание - Текст должен включать 140 или меньше символов. - Введите имя фрагмента - Приветствую, %s]]> - войти - Неудачная аторизация. - выйти - Я - Необходимо получить разрешение для доступа к хранилищу. - Не могу подключиться к серверу Google. Пожалуйста, проверьте ваше соединение. - You can run QPython script even it\'s in background if this mode is enabled. - Disable - QPython will restart to apply changes. - "Please grant the creat shortcut permission. " - Welcome to Use QPython - \ No newline at end of file diff --git a/qpython/src/main/res/values-v11/strings.xml b/qpython/src/main/res/values-v11/strings.xml deleted file mode 100644 index 55344e51920f3dcbb968d32ee2281e029d1571bf..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-v11/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/qpython/src/main/res/values-v21/strings.xml b/qpython/src/main/res/values-v21/strings.xml deleted file mode 100644 index 55344e51920f3dcbb968d32ee2281e029d1571bf..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-v21/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/qpython/src/main/res/values-w820dp/strings.xml b/qpython/src/main/res/values-w820dp/strings.xml deleted file mode 100644 index 55344e51920f3dcbb968d32ee2281e029d1571bf..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-w820dp/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/qpython/src/main/res/values-zh-rCN/strings.xml b/qpython/src/main/res/values-zh-rCN/strings.xml index e573e54eb44778420e51364ca3efea79927f77f6..71f2891a44bd09335942a43721a7d3548e9c92df 100644 --- a/qpython/src/main/res/values-zh-rCN/strings.xml +++ b/qpython/src/main/res/values-zh-rCN/strings.xml @@ -502,4 +502,9 @@ 悬浮窗 轻触屏幕中心打开/关闭闪光灯。 解压资源中 + zh + 指定命令行交互界面 + 命令行界面 + QPython初始化中 + 需要 diff --git a/qpython/src/main/res/values-zh-rTW/strings.xml b/qpython/src/main/res/values-zh-rTW/strings.xml deleted file mode 100644 index de86f36496f2fd4a0e4c44804fe2d8019fb6c901..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,299 +0,0 @@ - - - - %1$s - %1$s * - [%1$s] - 設置 - - 顯示 - 顯示行號 - 自動換行 - 更改文本字體 - 文字大小 - 顏色主題 - 快速滾動 - 在左側顯示的行號 - 如果某行對於屏幕太寬時,自動換行 - 如果有性能問題,不要選中 - - - “你想保存文檔?\n任何未保存更改將丟失。” - 保存 - 刪除 - 不保存 - 取消 - 文件名 - 搜索 - 歷史 - 打開為只讀 - QEdit 是一個文本編輯器,針對Android平板電腦和手機,除了文本編輯,它可以使安卓上的Python/HTML 開發更方便和有趣。 - - “QEdit 是一個偉大的腳本編輯器” - 文件已存在,請重命名 - 視頻{0} - 播放 - 沒有發現影片在本頁面 - 請首先安裝A8視頻播放器 - 下載並保存為 - QBrowser - - 分享 - 另存為片段 - 片段 - 片段已另存為: - 無法插入片段 - 請輸入正確的號碼 - 自動高亮 - 自動突出顯示,如果行數少於%s - QEdit 僅僅支持預覽或執行以下類型的文件\n.py,.SH,.lua,html,.md。 - 請安裝QLua 運行LUA 腳本 - 請安裝QPython 運行python 腳本 - OK - 聯繫 - 授權 - support@quseit.com - 打開郵件… - http://quseit.com" - "https://play.google.com/store/apps/details?id=%s" - “看看我們的其他應用程序” - “評價我們的應用程序\n留下您的反饋” - “請隨時聯繫我們不管是反饋,功能要求或錯誤報告等。” - “本軟件提供“原樣”,無任何明示或暗示的保證。不管在任何情況下,筆者應該對因使用本軟件所引起的任何損失負責。” - 設計授權 - “創建所有的圖標為這個應用程序,並屬於該應用程序的作者: QPython." - “有什麼新聞?” - 添加到字典 - Ale 鍵發送 ESC - 關閉Alt 鍵發送 ESC - 使用Alt 鍵發送ESC - 終端模擬器 - 選擇字體文件(.ttf) - 關閉終端 - 禁用鍵盤快捷鍵 - 打開/關閉軟鍵盤 - 插件刪除成功 - 插件安裝失敗 - 插件安裝完成 - 驗證PATH路徑 - 默認 UTF-8 - 終端類型 - 狀態欄 - 命令行 - 選擇插件 - 插件管理 - 屏幕方位 - 發送鼠標事件 - 初始命令 - 輸入方式 - HOME 文件夾 - 文本大小 - Fn 鍵 - 允許PATH後擴 - Ctrl 鍵 - 顏色 - 退出時關閉終端 - 返回鍵行爲 - 允許PATH前置 - 操作欄 - 文本 - 終端 - 是否將PATH中無效d路徑清除 - 是否將UTF-8模式設爲默認 - 設置 Shell 的終端類型 - 顯示/隱藏狀態欄 - 指定命令行使用的 Shell - 選擇屏幕方位行爲 - 只讀模式 - 文件 - - - 確定 - 關閉 - 沒有任何改變 - 意見反饋 - 關於 - 文件不存在 - m - 啟用Root權限 - 發送 - 沒有發現可用的攝像頭,請查看是否開啟攝像頭權限 - 沒有文件讀取權限 - - - python - QPY - 本機應用 - Root模式 - 點評 - 已禁用 - https://pypi.python.org/simple/ - 添加 - SL4A服務運行中 - 服務開關 - 警告 - 重置空間將刪除所有在/qpython下的文件,包括您新建的文件,確定重置嗎? - 庫文件 - 保存 - 另存為 - 打開 - File Name - 空白文件 - 確定 - 取消 - 新建項目 - 發生了某些錯誤 - 項目名 - 轉至某行 - 文件夾 - 新建腳本 - 請輸入文件名 - - 粘貼 - 下載失敗 - 終端快捷方式 - 在此處運行終端 - 查找命令 - 文字圖標 - 命令 - --示例=\"\"a\" - 製作文字圖標 - 選擇快捷方式文件 - 快捷方式標籤 - 終端快捷方式 - 輸入標籤文字 - - Crtl 鍵未設置 - Fn 鍵未設置 - Ctrl 和 Fn 鍵 - 複製全部 - 確定 - 操作欄行爲 - 返回鍵行爲 - 文字顏色 - Ctrl 鍵 - Fn 鍵 - 文字大小 - 輸入方式 - 初始化命令 - 屏幕方向事件 - 項目 - 腳本 - Shell - 狀態欄 - 終端類型 - 禁用弱鎖 - 禁用Wifi 鎖 - 編輯文字 - 編輯器 - 反饋 ... - 找不到郵件實例發送 - 複製終端 - 啓用Root模式 - 啓用Wifi 鎖 - 選擇主題 - 內存無法訪問 - 或在此輸入mPath - 文件選擇器 - FTP服務 - 幫助 - http://jackpal.github.com/Android-Terminal-Emulator/help/index.html - - 鍵盤 - 位置 - 更多 - 打開新終端 - 當前已是最新版本 - 設置PYPI - - 檢查更新 - 確定要關閉終端嗎? - 轉換失敗 - 課程 - [ %s ] - 刪除失敗 - 確定要刪除該文件嗎? - 該目錄不可用 - 下載最新安裝包 - 下載可能會消耗運營商流量,確認下載嗎? - 下載新的包 - 無法打開,該文件不存在 - 課程中心 - 文件夾名 - 獲取設置信息失敗 - 關於 - 密碼 - 運行狀態 - 端口號 - 重置內存空間 - 根目錄 - 顯示密碼 - 保持喚醒 - 用戶名 - 新建文件夾 - 該設備未進入Root模式 - 當前設備不支持OpenGL2 - 詳情 - 安裝 - 卸載 - 更新 - 屏幕 - 會話已結束 - 程序 - 項目 - 最近 - 重命名 - 重命名失敗 - 重置Terminal - 當前狀態將被重置 - 在後臺運行 - 保存當前修改嗎? - 運行日誌 : %s - 運行日誌 - - 二維碼 - 從二維碼中讀取代碼 - - 屏幕 - 腳本 - 選擇文字 - 發送郵件至… - 設置 - SL4A服務 - SL4A服務未啓動 - 特殊鍵 - Task - 使用鍵盤快捷鍵 - 解壓python文件失敗,可能影響部分功能使用 - 未命名文件夾 - 終端列表 - 終端 %1$d - 文件已存在,請重命名 - 社區 - 在後臺運行此控制臺? - 偏好 - 衆籌中%d%% - 支持 - 我的 - 課程簡介 - 展開 - 課程內容 - 初級課程 - 中級課程 - 高級課程 - 作者簡介 - 已有%s人支持 - 學習 - 收起 - 作者 - 評論&提問 - 衆籌 - 衆籌中 - 獲得幫助 - You can run QPython script even it\'s in background if this mode is enabled. - Disable - QPython will restart to apply changes. - "Please grant the creat shortcut permission. " - Welcome to Use QPython - - \ No newline at end of file diff --git a/qpython/src/main/res/values-zh-rTW/toasts.xml b/qpython/src/main/res/values-zh-rTW/toasts.xml deleted file mode 100644 index f937668ae77d60fa07c4748a53d6625400dd411a..0000000000000000000000000000000000000000 --- a/qpython/src/main/res/values-zh-rTW/toasts.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 文件不存在 -文件已經從最近編輯中移除 - diff --git a/qpython/src/main/res/values/arrays.xml b/qpython/src/main/res/values/arrays.xml index 152e6e1b11c50d68d62f6c9f2cf6997cea864127..df6e2d69c502396dccd12245c9359492c2e0ae76 100644 --- a/qpython/src/main/res/values/arrays.xml +++ b/qpython/src/main/res/values/arrays.xml @@ -226,7 +226,7 @@ py txt - kv + sh html htm js @@ -236,7 +236,7 @@ log c h - + conf diff --git a/qpython/src/main/res/values/defaults.xml b/qpython/src/main/res/values/defaults.xml index fa95f18046be65eed650d50389e6671945d09685..d349c63c1c0ea2f9d6526aaada0b4e634c2ec7b9 100644 --- a/qpython/src/main/res/values/defaults.xml +++ b/qpython/src/main/res/values/defaults.xml @@ -15,7 +15,8 @@ 0 false /system/bin/sh - - + cd $HOME && python bin/shell.py && exit + screen true true diff --git a/qpython/src/main/res/values/strings.xml b/qpython/src/main/res/values/strings.xml index e55a181c8c64c4f0a6e91558ba2b58f67bbb9a86..898d1338aae2052ce34ceb7b8f497af60d637ab8 100644 --- a/qpython/src/main/res/values/strings.xml +++ b/qpython/src/main/res/values/strings.xml @@ -1,9 +1,9 @@ - 1347438995.05 + + 1698258641 https://www.qpython.org/contributors.html projects QPYPI @@ -772,4 +772,9 @@ Float View Tap the center of the screen to turn on/off the flash light . Extracting Resources + en + Choose Shell Interface + Shell Interface + QPython Initializing + need diff --git a/qpython/src/main/res/xml/console_preferences.xml b/qpython/src/main/res/xml/console_preferences.xml index 52d53766baa0062110c789a9aabbe88fddcc8441..452d2e432eaf26dddcb744f9640352180a0c12e1 100644 --- a/qpython/src/main/res/xml/console_preferences.xml +++ b/qpython/src/main/res/xml/console_preferences.xml @@ -158,6 +158,12 @@ android:title="@string/title_shell_preference" android:summary="@string/summary_shell_preference" android:dialogTitle="@string/dialog_title_shell_preference" /> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/qpython/src/oh/java/org/qpython/qpy/main/activity/BaseActivity.java b/qpython/src/oh/java/org/qpython/qpy/main/activity/BaseActivity.java deleted file mode 100644 index 3ed0c8b15c1a3141baa2b5b5d77dcf219e4ffee9..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/activity/BaseActivity.java +++ /dev/null @@ -1,235 +0,0 @@ -package org.qpython.qpy.main.activity; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.util.ArrayMap; -import android.support.v7.app.AppCompatActivity; -import android.text.TextUtils; -import android.widget.Toast; - -import com.quseit.util.NAction; -import com.quseit.util.NUtil; -import com.umeng.analytics.MobclickAgent; - -import org.qpython.qpy.R; -import org.qpython.qpy.console.ShellTermSession; -import org.qpython.qpy.console.util.TermSettings; -import org.qpython.qpy.main.server.gist.TokenManager; -import org.renpy.android.ResourceManager; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Map; - - -public class BaseActivity extends AppCompatActivity { - // QPython interfaces - private static final int SCRIPT_CONSOLE_CODE = 1237; - private static final int PID_INIT_VALUE = -1; - private static final int DEFAULT_BUFFER_SIZE = 8192; - private static final int LOG_NOTIFICATION_ID = (int) System.currentTimeMillis(); - private ArrayList mArguments = new ArrayList<>(); - private InputStream mIn; - private OutputStream mOut; - private Map mActionMap = new ArrayMap<>(); - private TermSettings mSettings; - private ShellTermSession session; - - private boolean permissionGrant = true; - - protected static ShellTermSession createTermSession(Context context, TermSettings settings, String initialCommand, String path) { - ShellTermSession session = null; - try { - session = new ShellTermSession(context, settings, initialCommand, path); - session.setProcessExitMessage(context.getString(R.string.process_exit_message)); - - } catch (IOException e) { - e.printStackTrace(); - } - - return session; - } - - protected void toast(String content) { - Toast.makeText(this, content, Toast.LENGTH_SHORT).show(); - } - - @Override - protected void onResume() { - super.onResume(); - MobclickAgent.onPageStart(this.getLocalClassName()); - MobclickAgent.onResume(this); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - MobclickAgent.setDebugMode(true); - MobclickAgent.openActivityDurationTrack(false); - MobclickAgent.setScenarioType(this, MobclickAgent.EScenarioType.E_UM_NORMAL); - } - - @Override - protected void onPause() { - super.onPause(); - MobclickAgent.onPageEnd(this.getLocalClassName()); - MobclickAgent.onPause(this); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - PermissionAction action = mActionMap.get(requestCode); - if (action != null) { - if (grantResults.length > 0 && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { - action.onGrant(); - } else { - action.onDeny(); - } - - } - mActionMap.remove(action); - } - - - public final void checkPermissionDo(String[] permissions, PermissionAction action) { - if (Build.VERSION.SDK_INT >= 23) { - boolean granted = true; - for (String permission : permissions) { - int checkPermission = ContextCompat.checkSelfPermission(this, permission); - granted = checkPermission == PackageManager.PERMISSION_GRANTED; - } - if (!granted) { - int code = permissions.hashCode() & 0xffff; - mActionMap.put(code, action); - ActivityCompat.requestPermissions(this, permissions, code); - } else { - action.onGrant(); - } - } else { - action.onGrant(); - } - } - - - // feedback - public void onFeedback(String feedback) { - - String app = getString(R.string.app_name); - int ver = NUtil.getVersionCode(getApplicationContext()); - String subject = MessageFormat.format(getString(com.quseit.android.R.string.feeback_email_title), app, ver, Build.PRODUCT); - - String lastError = ""; - String code = NAction.getCode(getApplicationContext()); - File log = new File(FileUtils.getPath(App.getContext()) + "/" + code + "_last_err.log"); - if (log.exists()) { - lastError = com.quseit.util.FileHelper.getFileContents(log.getAbsolutePath()); - } - - String body = MessageFormat.format(getString(R.string.feedback_email_body), Build.PRODUCT, - Build.VERSION.RELEASE, Build.VERSION.SDK, lastError, feedback); - - Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + getString(R.string.ui_feedback_mail))); - - intent.putExtra(Intent.EXTRA_SUBJECT, subject); - intent.putExtra(Intent.EXTRA_TEXT, session == null ? body : session.getTranscriptText().trim()); - try { - startActivity(Intent.createChooser(intent, - getString(R.string.email_transcript_chooser_title))); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, - R.string.email_transcript_no_email_activity_found, - Toast.LENGTH_LONG) - .show(); - } - } - - protected boolean checkExpired(final String resource, String filesDir, String tag) { - ResourceManager resourceManager = new ResourceManager(this); - - String data_version = resourceManager.getString(resource + "_version"); - String disk_version = "0"; - - // If no version, no unpacking is necessary. - if (data_version == null) { - return false; - } - - // Check the current disk version, if any. - String disk_version_fn = filesDir + "/lib/" + tag + "_" + resource + ".version"; - - try { - byte buf[] = new byte[64]; - InputStream is = new FileInputStream(disk_version_fn); - int len = is.read(buf); - disk_version = new String(buf, 0, len); - is.close(); - } catch (Exception e) { - - disk_version = "0"; - //Mint.logException(e); - - } - - //LogUtil.d(TAG, "data_version:"+Math.round(Double.parseDouble(data_version))+"-disk_version:"+Math.round(Double.parseDouble(disk_version))+"-RET:"+(int)(Double.parseDouble(data_version)-Double.parseDouble(disk_version))); - if ((int) (Double.parseDouble(data_version) - Double.parseDouble(disk_version)) > 0 || disk_version.equals("0")) { - try { - FileOutputStream os = new FileOutputStream(disk_version_fn); - try { - os.write(data_version.getBytes()); - os.close(); - - } catch (IOException e) { - e.printStackTrace(); - //Mint.logException(e); - - } - - } catch (FileNotFoundException e) { - e.printStackTrace(); - //Mint.logException(e); - - } - - - return true; - } else { - return false; - } - } - - public interface PermissionAction { - void onGrant(); - - void onDeny(); - } - - public void ifLogin(Login afterLogin) { - if (!TextUtils.isEmpty(TokenManager.getToken())) { - afterLogin.process(); - } else { - Toast.makeText(this, R.string.login_first, Toast.LENGTH_SHORT).show(); - } - } - - public interface Login { - void process(); - } - -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java b/qpython/src/oh/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java deleted file mode 100644 index afd953e4c2aef555c25d6931ecd2abcf21efc348..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/activity/FundingPurchaseActivity.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.qpython.qpy.main.activity; - -import android.content.Context; -import android.content.Intent; -import android.databinding.DataBindingUtil; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.View; -import android.widget.Toast; - -//import com.hipipal.qpyplus.wxapi.PaymentStatus; -//import com.hipipal.qpyplus.wxapi.WXAPIManager; -//import com.hipipal.qpyplus.wxapi.WeixinPay; -import com.quseit.util.VeDate; - -import org.greenrobot.eventbus.Subscribe; -import org.json.JSONException; -import org.json.JSONObject; -import org.qpython.qpy.R; -import org.qpython.qpy.databinding.ActivityFundingPurchaseBinding; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.utils.UpdateHelper; - -import rx.Observer; - -/** - * Created by Hmei - * 1/30/18. - */ - -public class FundingPurchaseActivity extends PayActivity { - private static final String FUNDING_COUNT = "fundingCount"; - private ActivityFundingPurchaseBinding binding; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_funding_purchase); - initView(); - String[] count = getResources().getStringArray(R.array.funding_count_divider); - String[] prices = getResources().getStringArray(R.array.wechat_funding_price); - String price = null; - int fundingCount = getIntent().getIntExtra(FUNDING_COUNT, 0); - for (int i = 0; i < count.length; i++) { - if ((Integer.parseInt(count[i]) - fundingCount) > 0) { - price = prices[i]; - break; - } - } - if (TextUtils.isEmpty(price)) price = prices[prices.length - 1]; - binding.price.setText(getString(R.string.rmb, price)); - String finalPrice = price; -// binding.price.setOnClickListener(v -> payUtil.purchase(finalPrice, getIntent().getStringExtra(ARTICLE_ID), new Observer() { -// @Override -// public void onCompleted() { -// binding.pb.setVisibility(View.GONE); -// // 统计赞赏数据 -// JSONObject jsonObject = new JSONObject(); -// try { -// jsonObject.put("type", finalPrice); -// jsonObject.put("time", VeDate.getStringDateHourAsInt()); -// int percent = getIntent().getIntExtra(FUNDING_COUNT, 0); -// String[] fundingCount = getResources().getStringArray(R.array.funding_count_divider); -// jsonObject.put("crowdfunding", -// percent > (Integer.parseInt(fundingCount[1]) / Integer.parseInt(fundingCount[2])) ? 3 : -// percent > (Integer.parseInt(fundingCount[0]) / Integer.parseInt(fundingCount[1])) ? 2 : 1);//0 非众筹/ 1: 0-100 /2: 100-500/3 500-2000 -// jsonObject.put("articleId", getIntent().getStringExtra(ARTICLE_ID)); -// if (App.getUser() != null) { -// jsonObject.put("account", App.getUser().getEmail()); -// } -// } catch (JSONException e) { -// e.printStackTrace(); -// } -// UpdateHelper.submitIAPLog(FundingPurchaseActivity.this, mOrderNo, App.getGson().toJson(jsonObject)); -// } -// -// @Override -// public void onError(Throwable e) { -// Toast.makeText(FundingPurchaseActivity.this, R.string.conn_error, Toast.LENGTH_SHORT).show(); -// binding.pb.setVisibility(View.GONE); -// } -// -// @Override -// public void onNext(WeixinPay weixinPay) { -// WXAPIManager.wxPayReq(weixinPay); -// mOrderNo = weixinPay.out_trade_no; -// } -// })); - } - - public static void startSupport(Context context, String articleId, int fundingPercent) { - Intent starter = new Intent(context, FundingPurchaseActivity.class); - starter.putExtra(ARTICLE_ID, articleId); - starter.putExtra(FUNDING_COUNT, fundingPercent); - context.startActivity(starter); - } - - private void initView() { - setSupportActionBar(binding.lt.toolbar); - binding.lt.toolbar.setNavigationIcon(R.drawable.ic_back); - binding.lt.toolbar.setNavigationOnClickListener(v -> finish()); - setTitle(R.string.reward); - binding.tvThanks.setVisibility(View.VISIBLE); - binding.price.setVisibility(View.VISIBLE); - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/activity/PayActivity.java b/qpython/src/oh/java/org/qpython/qpy/main/activity/PayActivity.java deleted file mode 100644 index 89e16879ba40b1b3fbbe51bd60f9f17320086221..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/activity/PayActivity.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.qpython.qpy.main.activity; - -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; -import android.view.View; -import android.widget.Toast; - - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.qpython.qpy.R; -import org.qpython.qpy.main.service.PayUtil; - -import rx.Observer; - -/** - * Created by Hmei - * 1/31/18. - */ - -public class PayActivity extends AppCompatActivity { - protected static final String ARTICLE_ID = "article_id"; - protected String mOrderNo; - protected PayUtil payUtil; - - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - payUtil = new PayUtil(this); - EventBus.getDefault().register(this); - } - - @Subscribe - public void onPayResp(String msg) { -// if (msg.equals(PaymentStatus.SUCCESS)) { -// payUtil.checkOrderStatus(mOrderNo,new Observer() { -// @Override -// public void onCompleted() { -// -// } -// -// @Override -// public void onError(Throwable e) { -// e.printStackTrace(); -// if (findViewById(R.id.pb) != null) -// findViewById(R.id.pb).setVisibility(View.GONE); -// Toast.makeText(PayActivity.this, "支付失败!", Toast.LENGTH_SHORT).show(); -// } -// -// @Override -// public void onNext(PaymentStatus paymentStatus) { -// if (paymentStatus.isSuccess()) { -// if (findViewById(R.id.pb) != null) -// findViewById(R.id.pb).setVisibility(View.GONE); -// Toast.makeText(PayActivity.this, "感谢您的支持!", Toast.LENGTH_SHORT).show(); -// finish(); -// } else { -// payUtil.checkOrderStatus(mOrderNo,this); -// } -// } -// }); -// } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - EventBus.getDefault().unregister(this); - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/activity/PurchaseActivity.java b/qpython/src/oh/java/org/qpython/qpy/main/activity/PurchaseActivity.java deleted file mode 100644 index 8520aa061700f1785a21151917c80bd404bab7b1..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/activity/PurchaseActivity.java +++ /dev/null @@ -1,149 +0,0 @@ -package org.qpython.qpy.main.activity; - -import android.content.Context; -import android.content.Intent; -import android.databinding.DataBindingUtil; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -//import com.hipipal.qpyplus.wxapi.PaymentStatus; -//import com.hipipal.qpyplus.wxapi.WXAPIManager; -//import com.hipipal.qpyplus.wxapi.WeixinPay; -import com.quseit.util.ImageUtil; -import com.quseit.util.VeDate; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.json.JSONException; -import org.json.JSONObject; -import org.qpython.qpy.R; -import org.qpython.qpy.databinding.ActivityPurchaseBinding; -import org.qpython.qpy.databinding.ItemPriceBinding; -import org.qpython.qpy.main.adapter.MyViewHolder; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.app.User; -import org.qpython.qpy.main.service.PayUtil; -import org.qpython.qpy.main.widget.GridSpace; -import org.qpython.qpy.utils.UpdateHelper; - -import java.util.ArrayList; -import java.util.List; - -import rx.Observer; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; - -/** - * Weixin purchase activity - * Created by Hmei on 2017-07-19. - */ - -public class PurchaseActivity extends PayActivity { - private static String[] prices = new String[]{"6", "12", "18", "24", "30", "36"}; - - private ActivityPurchaseBinding binding; - private String articleId; - - public static void start(Context context, String articleId) { - Intent starter = new Intent(context, PurchaseActivity.class); - starter.putExtra(ARTICLE_ID, articleId); - context.startActivity(starter); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_purchase); - articleId = getIntent().getStringExtra(ARTICLE_ID); - initView(); - getPrices(); - } - - private void initView() { - setSupportActionBar(binding.lt.toolbar); - binding.lt.toolbar.setNavigationIcon(R.drawable.ic_back); - binding.lt.toolbar.setNavigationOnClickListener(v -> finish()); - setTitle(R.string.reward); - } - - private void getPrices() { - invalidateData(); - GridSpace gridSpace = new GridSpace(2, (int) ImageUtil.dp2px(16), false); - binding.list.setLayoutManager(new GridLayoutManager(PurchaseActivity.this, 2)); - binding.list.addItemDecoration(gridSpace); - binding.list.setAdapter(new ListAdapter(prices)); - } - - private void invalidateData() { - binding.tvThanks.setVisibility(View.VISIBLE); - binding.list.setVisibility(View.VISIBLE); - binding.noData.llRoot.setVisibility(View.GONE); - } - - private class ListAdapter extends RecyclerView.Adapter> { - String[] dataList; - - ListAdapter(String[] dataList) { - this.dataList = dataList; - } - - @Override - public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - ItemPriceBinding binding = DataBindingUtil.inflate(LayoutInflater.from(PurchaseActivity.this), R.layout.item_price, parent, false); - MyViewHolder holder = new MyViewHolder<>(binding.getRoot()); - holder.setBinding(binding); - return holder; - } - - @Override - public void onBindViewHolder(MyViewHolder holder, int position) { - ItemPriceBinding itemBinding = holder.getBinding(); - itemBinding.tvPrices.setText(dataList[position]); -// itemBinding.tvPrices.setOnClickListener(v -> payUtil.purchase(dataList[position], articleId, new Observer() { -// @Override -// public void onCompleted() { -// binding.pb.setVisibility(View.GONE); -// JSONObject jsonObject = new JSONObject(); -// try { -// jsonObject.put("type", dataList[position]); -// jsonObject.put("time", VeDate.getStringDateHourAsInt()); -// jsonObject.put("crowdfunding",0);//0 非众筹/ 1: 0-100 /2: 100-500/3 500-2000 -// jsonObject.put("articleId", articleId); -// if (App.getUser() != null) { -// jsonObject.put("account", App.getUser().getEmail()); -// } -// } catch (JSONException e) { -// e.printStackTrace(); -// } -// UpdateHelper.submitIAPLog(PurchaseActivity.this, mOrderNo, App.getGson().toJson -// (jsonObject)); -// } -// -// @Override -// public void onError(Throwable e) { -// e.printStackTrace(); -// Toast.makeText(PurchaseActivity.this, "connection error!", Toast.LENGTH_SHORT).show(); -// binding.pb.setVisibility(View.GONE); -// } -// -// @Override -// public void onNext(WeixinPay weixinPay) { -// WXAPIManager.wxPayReq(weixinPay); -// mOrderNo = weixinPay.out_trade_no; -// } -// })); - } - - @Override - public int getItemCount() { - return dataList.length; - } - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/activity/SignInActivity.java b/qpython/src/oh/java/org/qpython/qpy/main/activity/SignInActivity.java deleted file mode 100644 index fe4bc7b88d135d4df5b24e25e070d99ac26d0766..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/activity/SignInActivity.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.qpython.qpy.main.activity; - -import android.content.Intent; -import android.databinding.DataBindingUtil; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; -import android.text.Html; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - -//import com.hipipal.qpyplus.wxapi.WXAPIManager; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.qpython.qpy.R; -import org.qpython.qpy.databinding.ActivitySignInBinding; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.app.User; -import org.qpython.qpy.main.server.gist.loginScreen.LoginControler; -import org.qpython.qpy.main.server.gist.loginScreen.LoginView; - -/** - * SignIn - * Created by Hmei on 2017-08-04. - */ - -public class SignInActivity extends AppCompatActivity implements LoginView{ - private static final int RC_SIGN_IN = 54503; - - private ActivitySignInBinding binding; - private LoginControler mLoginControler; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_sign_in); - binding.textView3.setText(Html.fromHtml(getString(R.string.by_signing_in_you_agree_to_out_privacy_policy_term_of_service))); - initListener(); - EventBus.getDefault().register(this); - mLoginControler = new LoginControler(this); - } - - private void initListener() { - binding.textView3.setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.privacy_html))))); - binding.button2.setOnClickListener(v -> signIn()); - binding.button3.setOnClickListener(v -> finish()); - } - - @Subscribe - public void loginFail(String result) { - Toast.makeText(this, R.string.login_failed, Toast.LENGTH_SHORT).show(); - } - - @Subscribe - public void loginSuccess(User user) { - Log.d("SignInActivity", "loginSuccess"); - - } - - private void signIn() { - -// WXAPIManager.wxLogin(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - EventBus.getDefault().unregister(this); - mLoginControler.onDestroy(); - } - - @Override - public void showLoading() { - binding.progressBar2.setVisibility(View.VISIBLE); - } - - @Override - public void hideLoading() { - binding.progressBar2.setVisibility(View.GONE); - } - - @Override - public void showToast(String msg) { - Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); - } - - @Override - public void loginSuccess() { - setResult(RESULT_OK); - finish(); - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/activity/UserActivity.java b/qpython/src/oh/java/org/qpython/qpy/main/activity/UserActivity.java deleted file mode 100644 index 38bc570f97ecf21e18f8a83c54eea156b03a9853..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/activity/UserActivity.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.qpython.qpy.main.activity; - -import android.content.Context; -import android.content.Intent; -import android.databinding.DataBindingUtil; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.widget.Toast; - -import com.quseit.util.ImageDownLoader; - -import org.qpython.qpy.R; -import org.qpython.qpy.codeshare.CONSTANT; -import org.qpython.qpy.codeshare.ShareCodeUtil; -import org.qpython.qpy.databinding.ActivityUserBinding; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.app.CONF; - -import java.io.File; - -public class UserActivity extends AppCompatActivity { - ActivityUserBinding binding; - - public static void start(Context context) { - Intent starter = new Intent(context, UserActivity.class); - context.startActivity(starter); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_user); - setSupportActionBar(binding.toolbar); - setTitle(getString(R.string.me)); - binding.toolbar.setNavigationIcon(R.drawable.ic_back); - binding.toolbar.setNavigationOnClickListener(v -> finish()); - - ImageDownLoader.setImageFromUrl(this, binding.avatar, App.getUser().getAvatarUrl()); - binding.name.setText(App.getUser().getNick()); -// binding.email.setText(App.getUser().getEmail()); - - if (ShareCodeUtil.getInstance().getUsage() == -1) { - Toast.makeText(App.getContext(), "usage not init", Toast.LENGTH_SHORT).show(); - ShareCodeUtil.getInstance().initUsage(); - } else { - binding.usage.setText(getString(R.string.my_space, ShareCodeUtil.getInstance().getUsage())); - } - binding.logout.setOnClickListener(v -> logout()); - - initListener(); - } - - private void initListener() { - binding.myShareLayout.setOnClickListener(v -> MyGistActivity.startMyShare(this)); - } - - private void logout() { - new AlertDialog.Builder(this, R.style.MyDialog) - .setTitle(R.string.lout) - .setPositiveButton(R.string.yes, (dialog, which) -> { - App.setUser(null); - getPreferences(MODE_PRIVATE).edit().putBoolean(CONSTANT.IS_UPLOAD_INIT, false) - .putString(CONSTANT.CLOUDED_MAP, "") - .apply(); - finish(); - }) - .setNegativeButton(R.string.no, null) - .create() - .show(); - - File cloud_cache = new File(CONF.CLOUD_MAP_CACHE_PATH); - if (cloud_cache.exists()) { - cloud_cache.delete(); - } - } -} \ No newline at end of file diff --git a/qpython/src/oh/java/org/qpython/qpy/main/app/AppInit.java b/qpython/src/oh/java/org/qpython/qpy/main/app/AppInit.java deleted file mode 100644 index 812871d8185ea466b484949831c9bea1aa90e193..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/app/AppInit.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.qpython.qpy.main.app; - -import android.content.Context; -import android.util.Log; - -import org.qpython.qpy.BuildConfig; -import org.qpython.qpy.codeshare.ShareCodeUtil; -//import com.hipipal.qpyplus.wxapi.WXAPIManager; - -/** - * 文 件 名: AppInit - * 创 建 人: ZhangRonghua - * 创建日期: 2018/3/8 15:13 - * 修改时间: - * 修改备注: - */ - -public class AppInit { - - public static void init(Context context){ - //wx -// WXAPIManager.init(context); - ShareCodeUtil.getInstance(); - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/fragment/ExplorerFragment.java b/qpython/src/oh/java/org/qpython/qpy/main/fragment/ExplorerFragment.java deleted file mode 100644 index 706f08971feae1ec71514b7b1f7f2d73d3635595..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/fragment/ExplorerFragment.java +++ /dev/null @@ -1,444 +0,0 @@ -package org.qpython.qpy.main.fragment; - -import static com.quseit.util.FolderUtils.sortTypeByName; - -import android.content.Intent; -import android.databinding.DataBindingUtil; -import android.graphics.Color; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import com.quseit.util.FileHelper; -import com.quseit.util.FileUtils; -import com.quseit.util.ImageUtil; -import com.yanzhenjie.recyclerview.swipe.SwipeMenuCreator; -import com.yanzhenjie.recyclerview.swipe.SwipeMenuItem; - -import org.qpython.qpy.R; -import org.qpython.qpy.databinding.FragmentExplorerBinding; -import org.qpython.qpy.main.activity.NotebookActivity; -import org.qpython.qpy.main.activity.SignInActivity; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.app.CONF; -import org.qpython.qpy.texteditor.EditorActivity; -import org.qpython.qpy.texteditor.common.CommonEnums; -import org.qpython.qpy.texteditor.common.RecentFiles; -import org.qpython.qpy.texteditor.ui.adapter.FolderAdapter; -import org.qpython.qpy.texteditor.ui.adapter.bean.FolderBean; -import org.qpython.qpy.texteditor.ui.view.EnterDialog; -import org.qpython.qpy.texteditor.widget.crouton.Crouton; -import org.qpython.qpy.texteditor.widget.crouton.Style; -import org.qpython.qpy.utils.NotebookUtil; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - -public class ExplorerFragment extends Fragment { - private static final int REQUEST_SAVE_AS = 107; - private static final int REQUEST_HOME_PAGE = 109; - private static final int REQUEST_RECENT = 111; - private static final int LOGIN_REQUEST = 2741; - - private static final String TYPE = "type"; - - private int WIDTH = (int) ImageUtil.dp2px(60); - - private FragmentExplorerBinding binding; - private List folderList; - private FolderAdapter adapter; - private Map cloudedMap = new HashMap<>(); - - private boolean openable = true; // 是否可打开文件 - - private int type; - private String curPath; - - public static ExplorerFragment newInstance(int type) { - ExplorerFragment myFragment = new ExplorerFragment(); - - Bundle args = new Bundle(); - args.putInt(TYPE, type); - myFragment.setArguments(args); - - return myFragment; - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_explorer, null); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - binding = DataBindingUtil.bind(view); - type = getArguments().getInt(TYPE); - initView(); - initListener(); - initCloud(); - switch (type) { - case REQUEST_RECENT: - binding.rlPath.setVisibility(View.GONE); - break; - case REQUEST_SAVE_AS: - binding.ivNewFolder.setVisibility(View.VISIBLE); - break; - case REQUEST_HOME_PAGE: - binding.ivNewFolder.setVisibility(View.VISIBLE); - break; - } - } - - private void initView() { - SwipeMenuCreator swipeMenuCreator = (leftMenu, rightMenu, viewType) -> { -// SwipeMenuItem uploadItem = new SwipeMenuItem(getContext()) -// .setBackgroundColor(Color.parseColor("#FF4798F3")) -// .setImage(R.drawable.ic_cloud_upload) -// .setHeight(ViewGroup.LayoutParams.MATCH_PARENT) -// .setWidth(WIDTH); - - SwipeMenuItem renameItem = new SwipeMenuItem(getContext()) - .setBackgroundColor(Color.parseColor("#FF4BAC07")) - .setImage(R.drawable.ic_file_rename) - .setHeight(ViewGroup.LayoutParams.MATCH_PARENT) - .setWidth(WIDTH); - - SwipeMenuItem deleteItem = new SwipeMenuItem(getContext()) - .setBackgroundColor(Color.parseColor("#FFD14136")) - .setImage(R.drawable.ic_editor_filetree_close) - .setHeight(ViewGroup.LayoutParams.MATCH_PARENT) - .setWidth(WIDTH); - - switch (type) { - case REQUEST_RECENT: - rightMenu.addMenuItem(deleteItem); - break; - case REQUEST_SAVE_AS: - rightMenu.addMenuItem(deleteItem); - break; - case REQUEST_HOME_PAGE: - //rightMenu.addMenuItem(uploadItem); - rightMenu.addMenuItem(renameItem); - rightMenu.addMenuItem(deleteItem); - break; - } - }; - - folderList = new ArrayList<>(); - adapter = new FolderAdapter(folderList, getArguments().getInt(TYPE) == REQUEST_RECENT); - adapter.setCloudMap(cloudedMap); - binding.swipeList.setLayoutManager(new LinearLayoutManager(getContext())); - binding.swipeList.setSwipeMenuCreator(swipeMenuCreator); - openDir(CONF.ABSOLUTE_PATH); - } - - private void initListener() { - binding.ivNewFolder.setOnClickListener(v -> doNewDir()); - binding.prevFolder.setOnClickListener(v -> { - String parentPath = new File(curPath).getParent(); - - //if (parentPath.length()>= FileUtils.getQyPath().length()) { - openDir(parentPath); - //} - }); - binding.swipeList.setSwipeMenuItemClickListener(menuBridge -> { - binding.swipeList.smoothCloseMenu(); - switch (menuBridge.getPosition()) { - case 0: - renameFile(menuBridge.getAdapterPosition()); - break; - case 1: - deleteFile(menuBridge.getAdapterPosition()); - break; - } - }); - adapter.setClickListener(new FolderAdapter.Click() { - @Override - public void onItemClick(int position) { - FolderBean item = folderList.get(position); - if (item.getType().equals(CommonEnums.FileType.FILE)) { - if (!openable) { - Toast.makeText(getActivity(), R.string.cant_open, Toast.LENGTH_SHORT).show(); - return; - } - //判断文件类型 - int lastDot = item.getName().lastIndexOf("."); - if (lastDot != -1) { - String ext = item.getName().substring(lastDot + 1); - openFile(item.getFile(), ext); - } - } else { - openDir(item.getPath()); - } - } - - @Override - public void onLongClick(int position) { - binding.swipeList.smoothOpenRightMenu(position); - } - }); - - binding.swipeList.setAdapter(adapter); - } - - private void gotoSetting() { - org.qpython.qpy.main.activity.SettingActivity.startActivity(getActivity()); - } - private void openFile(File file, String ext) { - List textExts = Arrays.asList(getContext().getResources().getStringArray(R.array.text_ext)); - if (textExts.contains(ext)) { - EditorActivity.start(getContext(), Uri.fromFile(file)); - } else if (ext.equals("ipynb")) { - boolean notebookenable = NotebookUtil.isNotebookEnable(getActivity()); - if (notebookenable) { - NotebookActivity.start(getActivity(), file.getAbsolutePath(), false); - } else { - - new AlertDialog.Builder(getActivity(),R.style.MyDialog) - .setTitle(R.string.dialog_alert) - .setMessage(getString(R.string.ennable_notebook_first)) - .setNegativeButton(R.string.cancel, (dialog1, which) -> dialog1.dismiss()) - .setPositiveButton(R.string.ok, (dialog1, which) -> gotoSetting()) - .create() - .show(); - - //Toast.makeText(getActivity(), R.string.ennable_notebook_first, Toast.LENGTH_SHORT).show(); - } - } else { - FileHelper.openFile(getContext(),file.getPath(),ext); - } - } - - private void uploadFile(int adapterPosition) { - File file = folderList.get(adapterPosition).getFile(); - - // only support type in - String ext = ""; - if (file.getName().lastIndexOf(".") > 0) { - ext = file.getName().substring(file.getName().lastIndexOf(".") + 1); - } - boolean isSupport = false; - if (!file.isDirectory()) { - for (String s : getResources().getStringArray(R.array.support_file_ext)) { - if (s.equals(ext)) { - isSupport = true; - break; - } - } - } else { - isSupport = true; - } - - if (!isSupport) { - Toast.makeText(getContext(), R.string.not_support_type_hint, Toast.LENGTH_SHORT).show(); - return; - } - - // only available for already login user - if (App.getUser() == null) { - new AlertDialog.Builder(getActivity(), R.style.MyDialog) - .setTitle(R.string.need_login) - .setMessage(R.string.upload_login_hint) - .setNegativeButton(R.string.no, null) - .setPositiveButton(getString(R.string.login_now), (dialog, which) -> - startActivityForResult(new Intent(getActivity(), SignInActivity.class), LOGIN_REQUEST) - ) - .create() - .show(); - return; - } - - // upload - - File folder = folderList.get(adapterPosition).getFile(); - folderList.get(adapterPosition).setUploading(true); - adapter.notifyItemChanged(adapterPosition); - if (folder.isDirectory()) { - //ShareCodeUtil.getInstance().uploadFolder(folder, callback, 0); - } else { - //ShareCodeUtil.getInstance().uploadFile(folder, callback); - } - } - - private void renameFile(int adapterPosition) { - new EnterDialog(getContext()) - .setTitle(getString(R.string.rename)) - .setConfirmListener(name -> { - File oldFile = folderList.get(adapterPosition).getFile(); - File newFile = new File(oldFile.getParent(), name); - boolean renameSuc = oldFile.renameTo(newFile); - if (renameSuc) { - folderList.set(adapterPosition, new FolderBean(newFile)); - adapter.notifyItemChanged(adapterPosition); - return true; - } else { - Toast.makeText(getActivity(), R.string.rename_fail, Toast.LENGTH_SHORT).show(); - return false; - } - }) - .setText(folderList.get(adapterPosition).getName()) - .show(); - } - - private void deleteFile(int adapterPosition) { - switch (type) { - case REQUEST_RECENT: - RecentFiles.removePath(folderList.get(adapterPosition).getPath()); - folderList.remove(adapterPosition); - adapter.notifyDataSetChanged(); - break; - case REQUEST_HOME_PAGE: - case REQUEST_SAVE_AS: - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.MyDialog); - builder.setTitle(R.string.warning) - .setMessage(R.string.delete_file_hint) - .setNegativeButton(R.string.no, null) - .setPositiveButton(R.string.yes, (dialog, which) -> { - FileHelper.clearDir(folderList.get(adapterPosition).getFile().getAbsolutePath(), 0, true); - folderList.remove(adapterPosition); - adapter.notifyItemRemoved(adapterPosition); - adapter.notifyDataSetChanged(); - }) - .show(); - break; - } - } - - private void openDir(String dirPath) { - if (type == REQUEST_RECENT) { - folderList.clear(); - for (String path : RecentFiles.getRecentFiles()) { - folderList.add(new FolderBean(new File(path))); - } - - if (folderList.size() == 0) { - binding.emptyHint.setVisibility(View.VISIBLE); - } else { - binding.emptyHint.setVisibility(View.GONE); - adapter.notifyDataSetChanged(); - } - } else { - File dir = new File(dirPath); - if (dir.exists()) { - File[] files = dir.listFiles(); - if (files != null) { - Arrays.sort(files, sortTypeByName); - folderList.clear(); - for (File file : files) { - //if (!file.getName().startsWith(".")) { - folderList.add(new FolderBean(file)); - //} - } - adapter.notifyDataSetChanged(); - binding.tvPath.setText(dirPath); - curPath = dirPath; - adapter.notifyDataSetChanged(); - - } - } else { - Toast.makeText(getContext(), R.string.file_not_exists, Toast.LENGTH_SHORT).show(); - } - } - } - - private void doNewDir() { - new EnterDialog(getContext()) - .setTitle(getString(R.string.new_folder)) - .setHint(getString(R.string.folder_name)) - .setConfirmListener(name -> { - File dirN = new File(curPath, name.equals("") ? getString(R.string.untitled_folder) : name); - if (dirN.exists()) { - Crouton.makeText(getActivity(), getString(R.string.toast_folder_exist), Style.ALERT).show(); - return false; - } else { - if (dirN.mkdirs()) { - openDir(curPath + "/" + name); - } else { - Toast.makeText(getContext(), R.string.mkdir_fail, Toast.LENGTH_SHORT).show(); - } - return true; - } - }) - .show(); - } - - public void backToPrev() { - - String qpyDir = CONF.ABSOLUTE_PATH;//FileUtils.getQyPath()+"/qpython"; - - if (curPath == null || qpyDir.equals(curPath) || FileUtils.getQyPath().equals(curPath)) { - getActivity().finish(); - } else { - String parentPath = new File(curPath).getParent(); - openDir(parentPath); - } - } - - private void updateClouded(File file) { - cloudedMap.put(file.getAbsolutePath(), true); - if (file.isDirectory()) { - for (File file1 : file.listFiles()) { - if (file1.isDirectory()) { - updateClouded(file1); - } else { - cloudedMap.put(file.getAbsolutePath(), true); - } - } - } - } - - public String getCurPath() { - return curPath; - } - - private void initCloud() { - if (App.getUser() == null) { - return; - } - //获取云端脚本 - - } - - private void savePath(String path) { - String[] paths = path.split("/"); - StringBuffer realPath = new StringBuffer(CONF.ABSOLUTE_PATH); - for (String p : paths) { - if (!TextUtils.isEmpty(p)) { - realPath.append("/"); - realPath.append(p); - if (!p.equals("projects") && !p.equals("scripts") && !cloudedMap.keySet().contains(realPath.toString())) - cloudedMap.put(realPath.toString(), true); - } - } - } - - public void deleteCloudedMap(String absolutePath) { - if (cloudedMap.containsKey(absolutePath)) { - cloudedMap.remove(absolutePath); - adapter.notifyDataSetChanged(); - } - } - - public void updateCloudedFiles(Map map) { - if (map != null) { - cloudedMap.putAll(map); - } - adapter.notifyDataSetChanged(); - } -} -// GIT TEST \ No newline at end of file diff --git a/qpython/src/oh/java/org/qpython/qpy/main/fragment/MyProjectFragment.java b/qpython/src/oh/java/org/qpython/qpy/main/fragment/MyProjectFragment.java deleted file mode 100644 index 1fe9726be7b725700f2c868df46e3a4c054e71bb..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/fragment/MyProjectFragment.java +++ /dev/null @@ -1,277 +0,0 @@ -package org.qpython.qpy.main.fragment; - -import android.content.SharedPreferences; -import android.databinding.DataBindingUtil; -import android.graphics.Color; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.text.Html; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import com.google.gson.reflect.TypeToken; -import com.quseit.util.DateTimeHelper; -import com.quseit.util.ImageUtil; -import com.yanzhenjie.recyclerview.swipe.SwipeMenuCreator; -import com.yanzhenjie.recyclerview.swipe.SwipeMenuItem; - -import org.qpython.qpy.R; -import org.qpython.qpy.codeshare.ShareCodeUtil; -import org.qpython.qpy.codeshare.pojo.CloudFile; -import org.qpython.qpy.databinding.FragmentRefreshRvBinding; -import org.qpython.qpy.main.adapter.CloudScriptAdapter; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.app.CONF; -import org.qpython.qpy.main.event.ShareCodeCallback; -import org.qpython.qpy.texteditor.TedLocalActivity; -import org.qpython.qpysdk.QPyConstants; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; - -import static android.content.Context.MODE_PRIVATE; -import static org.qpython.qpy.codeshare.CONSTANT.CLOUDED_MAP; -import static org.qpython.qpy.codeshare.CONSTANT.IS_UPLOAD_INIT; - -public class MyProjectFragment extends Fragment { - private int WIDTH = (int) ImageUtil.dp2px(60); - private FragmentRefreshRvBinding binding; - private CloudScriptAdapter adapter; - private List scriptList = new ArrayList<>(); - private boolean isSaverOn; - public boolean isLoading; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - binding = DataBindingUtil.bind(LayoutInflater.from(getContext()).inflate(R.layout.fragment_refresh_rv, null)); - return binding.getRoot(); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - scriptList = new ArrayList<>(); - adapter = new CloudScriptAdapter(scriptList); - isSaverOn = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean(getString(R.string.key_saver), true); - initView(); - initListener(); - retry(!isSaverOn); - } - - @Override - public void onResume() { - super.onResume(); - if (adapter != null) { - adapter.notifyDataSetChanged(); - } - } - - @Override - public void onStop() { - super.onStop(); - locatedCloud(scriptList); - } - - public void retry(boolean forceRefresh) { - if (scriptList != null && adapter != null) { - scriptList.clear(); - adapter.notifyDataSetChanged(); - } - startProgressBar(); - isLoading = true; - // TODO: 2017/12/4 获取云端脚本 - - } - - public void notifyDataSetChange(){ - if (adapter!=null){ - adapter.notifyDataSetChanged(); - } - } - - /** - * 保存云端文件目录到本地 - */ - private void locatedCloud(List cloudFiles) { - if (cloudFiles.size() > 0) { - HashMap clouded = new HashMap<>(); - for (CloudFile cloudFile : cloudFiles) { - if (cloudFile.getPath().contains("/projects/")) { - clouded.put(CONF.ABSOLUTE_PATH + "/projects/" + cloudFile.getProjectName(), true); - } - clouded.put(CONF.ABSOLUTE_PATH + cloudFile.getPath(), true); - } - SharedPreferences sp = getActivity().getPreferences(MODE_PRIVATE); - Type type = new TypeToken>() { - }.getType(); - sp.edit().putBoolean(IS_UPLOAD_INIT, true) - .putString(CLOUDED_MAP, App.getGson().toJson(clouded, type)).apply(); - } - } - - private void startProgressBar() { - binding.progressBar.setVisibility(View.VISIBLE); - binding.netError.setVisibility(View.GONE); - Observable.just(null) - .delay(5, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext(o -> showNetErrorText()) - .subscribe(); - } - - private void showEmpty() { - if (binding.progressBar.getVisibility() == View.VISIBLE) { - binding.progressBar.setVisibility(View.GONE); - binding.netError.setText(R.string.cloud_empty_hint); - binding.netError.setVisibility(View.VISIBLE); - } - } - - private void showNetErrorText() { - if (binding.progressBar.getVisibility() == View.VISIBLE) { - binding.progressBar.setVisibility(View.GONE); - binding.netError.setText(R.string.net_lagging); - binding.netError.setVisibility(View.VISIBLE); - } - } - - private void initView() { - SwipeMenuCreator swipeMenuCreator = (leftMenu, rightMenu, viewType) -> { - SwipeMenuItem deleteItem = new SwipeMenuItem(getContext()) - .setBackgroundColor(Color.parseColor("#FFD14136")) - .setImage(R.drawable.ic_editor_filetree_close) - .setHeight(ViewGroup.LayoutParams.MATCH_PARENT) - .setWidth(WIDTH); - - SwipeMenuItem downloadItem = new SwipeMenuItem(getContext()) - .setBackgroundColor(Color.parseColor("#FF4798F3")) - .setImage(R.drawable.ic_cloud_download) - .setHeight(ViewGroup.LayoutParams.MATCH_PARENT) - .setWidth(WIDTH); - rightMenu.addMenuItem(downloadItem); - rightMenu.addMenuItem(deleteItem); - }; - binding.swipeList.setLayoutManager(new LinearLayoutManager(getContext())); - binding.swipeList.setSwipeMenuCreator(swipeMenuCreator); - } - - private void initListener() { - binding.netError.setOnClickListener(v -> retry(true)); - binding.swipeList.setSwipeMenuItemClickListener(menuBridge -> { - menuBridge.closeMenu(); - switch (menuBridge.getPosition()) { - case 0: - CloudFile cloudFile = scriptList.get(menuBridge.getAdapterPosition()); - String path; - path = CONF.ABSOLUTE_PATH + cloudFile.getPath(); - File file = new File(path); - if (file.exists()) { -// new AlertDialog.Builder(getContext(), R.style.MyDialog) -// .setTitle(R.string.override_hint) -// .setMessage(Html.fromHtml(getString(R.string.conflict_hint, -// cloudFile.getUploadTime(), -// DateTimeHelper.AGO_FULL_DATE_FORMATTER.format(new Date(file.lastModified()))))) -// .setNegativeButton(R.string.no, null) -// .setPositiveButton(R.string.yes, (dialog, which) -> writeFile(file, cloudFile, menuBridge.getAdapterPosition())) -// .create() -// .show(); - } else { - //create file - File dir = new File(path.substring(0, path.lastIndexOf("/"))); - if (!dir.exists()) { - dir.mkdirs(); - } - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } - writeFile(file, cloudFile, menuBridge.getAdapterPosition()); - } - break; - case 1: - // delete - scriptList.get(menuBridge.getAdapterPosition()).setUploading(true); - adapter.notifyItemChanged(menuBridge.getAdapterPosition()); - // TODO: 2017/12/4 删除上传的脚本文件 -// ShareCodeUtil.getInstance().deleteUploadScript(scriptList.get(menuBridge.getAdapterPosition()), new LeancloudStorage.SaveFileCallback() { -// @Override -// public void onSuccess(int code) { -// Toast.makeText(getContext(), R.string.delete_remote_suc, Toast.LENGTH_SHORT).show(); -// adapter.notifyItemRemoved(menuBridge.getAdapterPosition()); -// ((TedLocalActivity) getActivity()).deleteCloudFile(scriptList.get(menuBridge.getAdapterPosition()).getAbsolutePath()); -// scriptList.remove(menuBridge.getAdapterPosition()); -// } -// -// @Override -// public void onFail(String msg) { -// Toast.makeText(getContext(), "删除出错", Toast.LENGTH_SHORT).show(); -// } -// }); - break; - } - }); - adapter.setCallback((position) -> binding.swipeList.smoothOpenRightMenu(position)); - binding.swipeList.setAdapter(adapter); - } - - private void writeFile(File file, CloudFile cloudFile, int adapterPosition) { - cloudFile.setUploading(true); - adapter.notifyDataSetChanged(); -// ShareCodeUtil.getInstance().downloadFile(cloudFile.getFileObject().getFileId(), new LeancloudStorage.DownloadCallback() { -// @Override -// public void onSuccess(String content) { -// try { -// FileWriter writer = new FileWriter(file, false); -// writer.write(content); -// writer.close(); -// } catch (IOException e) { -// e.printStackTrace(); -// cloudFile.setUploading(false); -// adapter.notifyDataSetChanged(); -// Toast.makeText(getContext(), R.string.override_fail_hint, Toast.LENGTH_SHORT).show(); -// } -// Toast.makeText(getContext(), R.string.file_downloaded, Toast.LENGTH_SHORT).show(); -// cloudFile.setUploading(false); -// adapter.notifyItemChanged(adapterPosition); -// } -// -// @Override -// public void onFail(String msg) { -// -// } -// -// @Override -// public void onProgress(int progress) { -// -// } -// }); - } - - public void needRefresh(boolean isNewUpload) { - if (binding == null) { - // not init yet - return; - } - if (isNewUpload) { - retry(!isSaverOn); - } - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/main/service/PayUtil.java b/qpython/src/oh/java/org/qpython/qpy/main/service/PayUtil.java deleted file mode 100644 index 273081a33dcf33eee2bee126152d53a3749354c4..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/main/service/PayUtil.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.qpython.qpy.main.service; - -import android.app.Activity; -import android.content.Intent; -import android.view.View; -import android.widget.Toast; - -//import com.hipipal.qpyplus.wxapi.PaymentStatus; -//import com.hipipal.qpyplus.wxapi.WXAPIManager; -//import com.hipipal.qpyplus.wxapi.WeixinPay; - -import org.qpython.qpy.R; -import org.qpython.qpy.main.activity.PayActivity; -import org.qpython.qpy.main.activity.SignInActivity; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.app.User; -import org.qpython.qpy.main.server.MySubscriber; - -import java.util.ArrayList; -import java.util.List; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; - -/** - * Created by Hmei - * 1/31/18. - */ - -public class PayUtil { - private static final String GET_ORDER_URL = "https://api.wegox.net/create_order"; - public static final String CHECK_ORDER_URL = "https://api.wegox.net/check_order"; - private List mSubscriptions = new ArrayList<>(); - - private int checkCount = 0; - private Activity context; - - public PayUtil(Activity context) { - this.context = context; - } - - public void initIAP(PayCallback callback) { - callback.doAfterConn(); - } - - /** - * 赞赏 - */ -// public void purchase(String price, String articleId, Observer callback) { -// User user = App.getUser(); -// if (user == null) { -// context.startActivity(new Intent(context, SignInActivity.class)); -// return; -// } -// String userName = user.getUserName(); -// String userId = user.getUserId(); -// WXAPIManager.getWXService().requestPay(GET_ORDER_URL, price, userName, userId, articleId) -// .subscribeOn(Schedulers.io()) -// .subscribeOn(AndroidSchedulers.mainThread()) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe(callback); -// } - - /** - * 查询订单 - */ -// public void checkOrderStatus(String mOrderNo, Observer callback) { -// checkCount++; -// if (checkCount < 31) { -// Subscription subscription = WXAPIManager.getWXService().checkStatus(CHECK_ORDER_URL, mOrderNo) -// .subscribeOn(Schedulers.io()) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe(callback); -// mSubscriptions.add(subscription); -// } else { -// if (context.findViewById(R.id.pb) != null) -// context.findViewById(R.id.pb).setVisibility(View.GONE); -// Toast.makeText(context, "支付失败!", Toast.LENGTH_SHORT).show(); -// } -// } - - public void unbindPayService() { - for (Subscription subscription : mSubscriptions) { - if (subscription != null && subscription.isUnsubscribed()) { - subscription.unsubscribe(); - } - } - } - - public interface PayCallback { - void doAfterConn(); - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/sharecode/ShareCodeUtil.java b/qpython/src/oh/java/org/qpython/qpy/sharecode/ShareCodeUtil.java deleted file mode 100644 index 4e2aabaa82fbb24b1a1b76bcae758093520f5c18..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/sharecode/ShareCodeUtil.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.qpython.qpy.codeshare; - -import android.app.Activity; -import android.content.SharedPreferences; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.widget.Toast; - -import com.google.gson.reflect.TypeToken; -import com.quseit.util.ACache; -import com.quseit.util.FileHelper; -import com.quseit.util.NetStateUtil; - -import org.qpython.qpy.R; -import org.qpython.qpy.codeshare.pojo.CloudFile; -import org.qpython.qpy.codeshare.pojo.Gist; - -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.event.ShareCodeCallback; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - - -import static org.qpython.qpy.main.server.CacheKey.CLOUD_FILE; - -/** - * FireBase database util - * Created by Hmei on 2017-08-10. - */ - -public class ShareCodeUtil { - - public static final String USAGE = "usage"; - public static final String PROJECT = "project"; - public static final String SCRIPT = "script"; - public static final String PROJECT_PATH = "/projects/"; - public static final String SCRIPTS_PATH = "/scripts/"; - - private static final int MAX_FILE = 100; - - private SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(App.getContext()); - private int currentFileCount = -1; - private Handler mHandler = new Handler(App.getContext().getMainLooper()); - - private ShareCodeUtil() { - if (App.getUser() != null) { - initUsage(); - } - } - - public static ShareCodeUtil getInstance() { - return ShareCodeHolder.INSTANCE; - } - - /** - * 上传文件夹 - */ - - /** - * 管理文件内存 - */ - private List getCacheFile() { - String content = ACache.get(App.getContext()).getAsString(CLOUD_FILE); - List cloudFiles = App.getGson().fromJson(content, new TypeToken>() { - }.getType()); - return cloudFiles == null ? new ArrayList<>() : cloudFiles; - } - - private void saveCacheFile(List cloudFiles) { - ACache.get(App.getContext()).put(CLOUD_FILE, App.getGson().toJson(cloudFiles)); - } - - private void saveCacheFile(CloudFile cloudFile) { - List list = getCacheFile(); - if (!list.contains(cloudFile)) { - list.add(cloudFile); - } - saveCacheFile(list); - } - - private void deleteCacheFile(CloudFile cloudFile) { - List list = getCacheFile(); - if (list.contains(cloudFile)) { - list.remove(cloudFile); - } - saveCacheFile(list); - } - - private void clearCacheFile() { - ACache.get(App.getContext()).put(CLOUD_FILE, ""); - } - - /** - * 初始化用户文件空间 - */ - public void initUsage() { - // TODO - } - - public int changeUsage(int change) { - currentFileCount += change; - sharedPreferences.edit().putInt(USAGE, currentFileCount).apply(); - return currentFileCount; - } - - public int getUsage() { - return currentFileCount; - } - - private boolean hasSpace(int waiting) { - if (currentFileCount < 0) { - Toast.makeText(App.getContext(), "usage not init", Toast.LENGTH_SHORT).show(); - return false; - } else { - return currentFileCount + waiting < MAX_FILE; - } - } - - public interface CommentCallback { - void commentBean(Gist.CommentBean comment); - } - - private static class ShareCodeHolder { - private static final ShareCodeUtil INSTANCE = new ShareCodeUtil(); - } - - - private boolean isConnect() { - if (!NetStateUtil.isConnected(App.getContext())) { - Toast.makeText(App.getContext(), "connection error!", Toast.LENGTH_SHORT).show(); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/qpython/src/oh/java/org/qpython/qpy/sharecode/pojo/CloudFile.java b/qpython/src/oh/java/org/qpython/qpy/sharecode/pojo/CloudFile.java deleted file mode 100644 index 6ffa7f6e95bea4a476a84392f91d27bff3fa0d75..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/sharecode/pojo/CloudFile.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.qpython.qpy.codeshare.pojo; - - -import org.qpython.qpy.main.app.CONF; -import org.qpython.qpysdk.QPyConstants; - -import java.io.Serializable; - -import static org.qpython.qpy.codeshare.ShareCodeUtil.PROJECT; -import static org.qpython.qpy.codeshare.ShareCodeUtil.PROJECT_PATH; -import static org.qpython.qpy.codeshare.ShareCodeUtil.SCRIPT; -import static org.qpython.qpy.codeshare.ShareCodeUtil.SCRIPTS_PATH; - -public class CloudFile implements Serializable { - private String projectName; - private String path; - private String uploadTime; - private String name; - private String content; - private String key; - private boolean uploading; - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public String getUploadTime() { - return uploadTime; - } - - public void setUploadTime(String uploadTime) { - this.uploadTime = uploadTime; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public long getContentSize() { - return content.length(); - } - - public String getProjectName() { - return projectName; - } - - public void setProjectName(String projectName) { - this.projectName = projectName; - } - - public boolean isUploading() { - return uploading; - } - - public void setUploading(boolean uploading) { - this.uploading = uploading; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String getAbsolutePath() { - return CONF.ABSOLUTE_PATH + getPath(); - } -} diff --git a/qpython/src/oh/java/org/qpython/qpy/texteditor/TedLocalActivity.java b/qpython/src/oh/java/org/qpython/qpy/texteditor/TedLocalActivity.java deleted file mode 100644 index 501cdfc9aa11bb0e7208b9492de9063b015bd690..0000000000000000000000000000000000000000 --- a/qpython/src/oh/java/org/qpython/qpy/texteditor/TedLocalActivity.java +++ /dev/null @@ -1,309 +0,0 @@ -package org.qpython.qpy.texteditor; - - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.databinding.DataBindingUtil; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.view.MotionEvent; -import android.view.View; -import android.widget.EditText; -import android.widget.Toast; - -import org.qpython.qpy.R; -import org.qpython.qpy.databinding.ActivityLocalBinding; -import org.qpython.qpy.main.activity.SignInActivity; -import org.qpython.qpy.main.app.App; -import org.qpython.qpy.main.fragment.ExplorerFragment; -import org.qpython.qpy.main.fragment.LocalFragment; -import org.qpython.qpy.main.fragment.MyProjectFragment; -import org.qpython.qpy.utils.NotebookUtil; - -import java.io.File; - -public class TedLocalActivity extends AppCompatActivity { - public static final int REQUEST_SAVE_AS = 107; - public static final int REQUEST_OPEN = 108; - public static final int REQUEST_HOME_PAGE = 109; - public static final int REQUEST_RECENT = 111; - private static final int LOGIN_REQUEST_CODE = 4806; - - private static final String EXTRA_REQUEST_CODE = "request_code"; - private static final String FRAGMENT_EXPLORER = "explorer"; - private static final String FRAGMENT_CLOUD = "cloud"; - private static final String EXTRA_REQUEST_FN = "request_fn"; - - private ActivityLocalBinding binding; - - private Fragment firstPageFragment; - private MyProjectFragment myProjectFragment; - - private boolean isExplorer = true; - private boolean isNewUpload = false; - private String defaultFileName = ""; - - public static void start(Context context, int type) { - Intent starter = new Intent(context, TedLocalActivity.class); - starter.putExtra(EXTRA_REQUEST_CODE, type); - context.startActivity(starter); - } - - public static void start(Activity context, int type, int requestCode, String filename) { - Intent starter = new Intent(context, TedLocalActivity.class); - starter.putExtra(EXTRA_REQUEST_CODE, type); - starter.putExtra(EXTRA_REQUEST_FN, filename); - - context.startActivityForResult(starter, requestCode); - } - - public void finishForGetPath(String path) { - Intent intent = new Intent(); - intent.putExtra("path", path); - setResult(Activity.RESULT_OK, intent); - finish(); - } - - public void finishForOpen(String path, boolean isProj) { - Intent intent = new Intent(); - intent.putExtra("path", path); - intent.putExtra("isProj", isProj); - setResult(Activity.RESULT_OK, intent); - finish(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_local); - setSupportActionBar(binding.toolbar); - int type = getIntent().getIntExtra(EXTRA_REQUEST_CODE, REQUEST_HOME_PAGE); - initView(); - initListener(); - switch (type) { - case REQUEST_RECENT: - setTitle(R.string.recent); - binding.switchBtn.setVisibility(View.GONE); - firstPageFragment = ExplorerFragment.newInstance(type); - break; - case REQUEST_OPEN: - setTitle(R.string.open); - binding.switchBtn.setVisibility(View.GONE); - binding.explore.setVisibility(View.VISIBLE); - firstPageFragment = new LocalFragment(); - break; - case REQUEST_SAVE_AS: - setTitle(R.string.save_as); - binding.vsSave.getRoot().setVisibility(View.VISIBLE); - initSaveListener(); - String fn = getIntent().getStringExtra(EXTRA_REQUEST_FN); - if (fn!=null) { - binding.vsSave.etName.setText(fn); - } - - binding.switchBtn.setVisibility(View.GONE); - firstPageFragment = ExplorerFragment.newInstance(type); - break; - case REQUEST_HOME_PAGE: - setTitle(R.string.explorer); - firstPageFragment = ExplorerFragment.newInstance(type); - break; - } - - getSupportFragmentManager().beginTransaction() - .add(R.id.container, firstPageFragment, FRAGMENT_EXPLORER) - .commit(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - private void initView() { - binding.toolbar.setNavigationIcon(R.drawable.ic_back); - binding.toolbar.setNavigationOnClickListener(v -> finish()); - if (binding.switchBtn.getVisibility() == View.VISIBLE) { - myProjectFragment = new MyProjectFragment(); - } - } - - private void initListener() { - binding.switchBtn.setOnClickListener(v -> { - if (isExplorer) { - if (App.getUser() == null) { - startActivityForResult(new Intent(this, SignInActivity.class), LOGIN_REQUEST_CODE); - return; - } - binding.switchBtn.setImageResource(R.drawable.ic_folder_open); - binding.refresh.setVisibility(View.VISIBLE); - if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_CLOUD) == null) { - getSupportFragmentManager().beginTransaction().add(R.id.container, myProjectFragment, FRAGMENT_CLOUD).commit(); - getSupportFragmentManager().beginTransaction().hide(firstPageFragment).commit(); - } else { - getSupportFragmentManager().beginTransaction() - .hide(firstPageFragment) - .show(myProjectFragment) - .commit(); - } - myProjectFragment.needRefresh(isNewUpload); - isNewUpload = false; - if (!myProjectFragment.isLoading) { - myProjectFragment.notifyDataSetChange(); - } - } else { - binding.switchBtn.setImageResource(R.drawable.ic_cloud_list); - binding.refresh.setVisibility(View.GONE); - getSupportFragmentManager().beginTransaction() - .hide(myProjectFragment) - .show(firstPageFragment) - .commit(); - } - isExplorer = !isExplorer; - }); - - binding.refresh.setOnClickListener(v -> myProjectFragment.retry(true)); - - binding.explore.setOnClickListener(v -> { - start(this, REQUEST_HOME_PAGE); - finish(); - }); - } - - public void doSave(String fn) { - if (fn.length() == 0) { - Toast.makeText(getApplicationContext(), R.string.toast_filename_empty, Toast.LENGTH_SHORT).show(); - } else { - String filename = ((ExplorerFragment) firstPageFragment).getCurPath() + "/" + fn; - final File f = new File(filename); - if (f.exists()) { - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.MyDialog); - builder.setTitle(R.string.confirm) - .setMessage(R.string.editor_override) - .setPositiveButton(R.string.yes, (dialog, which) -> { - setSaveResult(f.getAbsolutePath()); - - }) - .setNegativeButton(R.string.cancel, ((dialog, which) -> nothing())) - .show(); - - - //Toast.makeText(this, R.string.file_exist_hint, Toast.LENGTH_SHORT).show(); - } else { - setSaveResult(f.getAbsolutePath()); - } - } - } - private void nothing() { - - - } - - protected boolean setSaveResult(String filepath) { - File f = new File(filepath); - if (f.getParentFile().canWrite()) { - finishForGetPath(filepath); - } else { - Toast.makeText(getApplicationContext(), R.string.toast_folder_cant_write, Toast.LENGTH_SHORT).show(); - } - return true; - } - - private void initSaveListener() { - binding.vsSave.btnSave.setOnClickListener(v -> doSave(binding.vsSave.etName.getText().toString())); - binding.vsSave.etName.setOnTouchListener((v, event) -> { - final int DRAWABLE_RIGHT = 2; - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - Drawable deleteText = ((EditText) v).getCompoundDrawables()[DRAWABLE_RIGHT]; - if (deleteText != null) { - if (event.getRawX() >= (v.getRight() - deleteText.getBounds().width())) { - ((EditText) v).setText(""); - return true; - } - } - break; - case MotionEvent.ACTION_UP: - v.performClick(); - break; - default: - break; - } - return false; - }); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK) { - switch (requestCode) { - case LOGIN_REQUEST_CODE: - binding.switchBtn.setImageResource(R.drawable.ic_folder_open); - if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_CLOUD) == null) { - getSupportFragmentManager().beginTransaction().add(R.id.container, myProjectFragment, FRAGMENT_CLOUD).commit(); - getSupportFragmentManager().beginTransaction().hide(firstPageFragment).commit(); - } else { - getSupportFragmentManager().beginTransaction() - .hide(firstPageFragment) - .show(myProjectFragment) - .commit(); - } - isExplorer = !isExplorer; - break; - } - } - } - - @Override - public void onBackPressed() { - if (firstPageFragment.isVisible() && firstPageFragment instanceof ExplorerFragment) { - ((ExplorerFragment) firstPageFragment).backToPrev(); - } else { - super.onBackPressed(); - } - } - -// public void deleteCloudFile(String path) { -// if (firstPageFragment instanceof ExplorerFragment) { -// ((ExplorerFragment) firstPageFragment).deleteCloudedMap(path); -// } -// } -// -// public void updateCloudFiles(List cloudFiles) { -// Map map = new HashMap<>(); -// for (CloudFile cloudFile : cloudFiles) { -// if (cloudFile.getPath().contains("/projects/")) { -// map.put(QPyConstants.ABSOLUTE_PATH + "/projects/" + cloudFile.getProjectName(), true); -// } -// map.put(QPyConstants.ABSOLUTE_PATH + cloudFile.getPath(), true); -// } -// ((ExplorerFragment) firstPageFragment).updateCloudedFiles(map); -// } -// -// public void setNewUpload() { -// isNewUpload = true; -// } -// -// /** -// * 保存云端文件目录到本地 -// */ -// public void locatedCloud(List cloudFiles) { -// if (cloudFiles.size() > 0) { -// Type type = new TypeToken>() { -// }.getType(); -// FileHelper.writeToFile(CONF.CLOUD_MAP_CACHE_PATH, App.getGson().toJson(cloudFiles, type)); -// } -// } - - @Override - protected void onDestroy() { - super.onDestroy(); - NotebookUtil.killServer(); - } -} diff --git a/qpython/src/oh/res/drawable-xxhdpi/ic_login.png b/qpython/src/oh/res/drawable-xxhdpi/ic_login.png deleted file mode 100644 index d45ba05f2038c492805ff812ee33b89dc1aed021..0000000000000000000000000000000000000000 Binary files a/qpython/src/oh/res/drawable-xxhdpi/ic_login.png and /dev/null differ diff --git a/qpython/src/oh/res/layout/activity_local.xml b/qpython/src/oh/res/layout/activity_local.xml deleted file mode 100644 index 4a03abe20fd4e0a66e83dc13fc7b211a84e82977..0000000000000000000000000000000000000000 --- a/qpython/src/oh/res/layout/activity_local.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - -