diff --git a/app/app/.gitignore b/app/app/.gitignore index 42afabfd2abebf31384ca7797186a27a4b7dbee8..a8f6ac7fc126301e975128e31f4e582accac1e20 100644 --- a/app/app/.gitignore +++ b/app/app/.gitignore @@ -1 +1,4 @@ -/build \ No newline at end of file +/build + +# Auto-generated by code in build.gradle. +src/main/assets/source \ No newline at end of file diff --git a/app/app/build.gradle b/app/app/build.gradle index 8cf92ef88795fbb0a2a518f59585b832de58862c..68903d5baca01512428f2730869667fb7e530ec1 100644 --- a/app/app/build.gradle +++ b/app/app/build.gradle @@ -1,7 +1,21 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' - id 'com.chaquo.python' + id 'com.chaquo.python' // Must come after com.android.application +} + +afterEvaluate { + def assetsSrcDir = "src/main/assets/source" + delete assetsSrcDir + mkdir assetsSrcDir + for (filename in ["src/main/python/chaquopy/demo/ui_demo.py", + "src/main/java/com/chaquo/python/demo/JavaDemoActivity.java"]) { + assert file(filename).exists() + copy { + from filename + into assetsSrcDir + } + } } android { @@ -15,51 +29,111 @@ android { } defaultConfig { - applicationId "com.example.privatestorage" + applicationId "com.chaquo.python.demo3" minSdk 27 targetSdk 32 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" - } + versionCode 20221111 + versionName "2022.11.11" python { version "3.9" buildPython "python3.9" + + // Android UI demo + pip { + install "Pygments==2.2.0" // Also used in Java API demo + } + staticProxy "chaquopy.demo.ui_demo" + + // Python unit tests + pip { + // In newer versions, importing murmurhash automatically imports and + // extracts murmurhash/mrmr.so, which would complicate the tests. + install "murmurhash==0.28.0" // Requires chaquopy-libcxx + } + staticProxy("chaquopy.test.static_proxy.basic", "chaquopy.test.static_proxy.header", + "chaquopy.test.static_proxy.method") + pyc { + // For testing bytecode compilation on device, and also to include test source + // code in stack traces. + src false + + // For testing bytecode compilation during build. + pip true + } } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + ndk { + abiFilters "armeabi-v7a", "arm64-v8a", "x86_64" } + + // Chaquopy generates extra internal-use constructors on static proxy classes. + lintOptions { + disable "ValidFragment" + } + + // Remove other languages imported from Android support libraries. + resConfigs "en" } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' + + // For testing with minifyEnabled (see release/README.md). + buildTypes { + releaseMinify { + initWith release + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } } - buildFeatures { - viewBinding true + + def keystore = file('../chaquo.jks') + if (keystore.exists()) { + signingConfigs { + config { + storeFile keystore + keyAlias 'key0' + keyPassword 'android' + storePassword 'android' + } + } + buildTypes.all { it.signingConfig signingConfigs.config } + } + + sourceSets { + for (path in [ + "/home/exarkun/Work/python/chaquopy/product/runtime/src/test", // Unit tests + "src/utils" // Files shared with pkgtest app + ]) { + main { + java { srcDir "$path/java" } + python { srcDir "$path/python" } + res { srcDir "$path/res" } + } + } } } dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + // appcompat version 1.2.0 is required to fix an incompatibility with WebView on API level + // 21 (https://stackoverflow.com/questions/41025200). + implementation 'androidx.appcompat:appcompat:1.2.0-beta01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' + implementation 'androidx.preference:preference:1.1.1' + implementation 'junit:junit:4.12' + implementation 'org.hamcrest:hamcrest-library:2.2' + + // I went to https://maven.google.com/web/index.html and searched for + // androidx.arch.core and a hit came up. I clicked on it and guessed that + // the testing library is what we need. I clicked on the newest version + // and it gave me an option to copy this string so I did and I put it + // HERE! + implementation 'androidx.arch.core:core-testing:2.1.0' - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + testImplementation 'junit:junit:4.12' } diff --git a/app/app/proguard-rules.pro b/app/app/proguard-rules.pro index 481bb434814107eb79d7a30b676d344b0df2f8ce..9b00913ae755853e788dee8cdefa2aaa943f0331 100644 --- a/app/app/proguard-rules.pro +++ b/app/app/proguard-rules.pro @@ -1,21 +1,26 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - # Uncomment this to preserve the line number information for # debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +# Android UI demo +-keep class chaquopy.demo.ui_demo.** { *; } +-keep class androidx.appcompat.app.** { *; } +-keep class androidx.core.app.** { *; } +-keep class androidx.fragment.app.** { *; } +-keep class androidx.preference.** { *; } + +# Java unit tests +-keep class com.chaquo.java.** { *; } +-keep class org.junit.** { *; } + +# Python unit tests +-keep class package1.** { *; } # TestImport +-keepattributes Exceptions # TestProxy.test_exception +-keep class chaquopy.test.static_proxy.** { *; } # TestStaticProxy diff --git a/app/app/src/androidTest/java/com/example/privatestorage/ExampleInstrumentedTest.kt b/app/app/src/androidTest/java/com/example/privatestorage/ExampleInstrumentedTest.kt deleted file mode 100644 index 5f5382757f2aaaf4bfc25849509d5ca37e2e9b6b..0000000000000000000000000000000000000000 --- a/app/app/src/androidTest/java/com/example/privatestorage/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.privatestorage - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.example.privatestorage", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index b2390170931626c7d88e2fe46e9d5e682e6ed38d..f7c8caf28178d9ee6634b0ffc8b32db8f7a164d5 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -1,29 +1,45 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.example.privatestorage"> +<manifest package="com.chaquo.python.demo" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.VIBRATE"/> <application + android:name="com.chaquo.python.utils.App" android:allowBackup="true" - android:dataExtractionRules="@xml/data_extraction_rules" - android:fullBackupContent="@xml/backup_rules" - android:icon="@mipmap/ic_launcher" + android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.PrivateStorage" - tools:targetApi="31"> - <activity - android:name=".MainActivity" - android:exported="true" + android:theme="@style/AppTheme" + tools:ignore="AllowBackup"> + <activity android:name="com.chaquo.python.utils.MainActivity" android:label="@string/app_name" - android:theme="@style/Theme.PrivateStorage.NoActionBar"> + android:exported="true"> <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <!-- Contrary to documentation, singleTask does not always create a new task: + https://issuetracker.google.com/issues/36921210--> + <activity android:name="com.chaquo.python.utils.ReplActivity" + android:label="@string/repl" + android:launchMode="singleInstance"> + </activity> + <activity android:name="chaquopy.demo.ui_demo.UIDemoActivity" + android:label="@string/ui_demo"> + </activity> + <activity android:name=".JavaDemoActivity" + android:label="@string/java_demo"> + </activity> + <activity android:name=".JavaTestActivity" + android:label="@string/java_unit_test"> + </activity> + <activity android:name="com.chaquo.python.utils.PythonTestActivity" + android:label="@string/python_unit_test"> + </activity> </application> </manifest> \ No newline at end of file diff --git a/app/app/src/main/java/com/chaquo/python/demo/JavaDemoActivity.java b/app/app/src/main/java/com/chaquo/python/demo/JavaDemoActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..644aaa8828580f97487d19802808a2f8f48688f4 --- /dev/null +++ b/app/app/src/main/java/com/chaquo/python/demo/JavaDemoActivity.java @@ -0,0 +1,63 @@ +package com.chaquo.python.demo; + +import android.content.*; +import android.os.*; +import android.util.*; +import android.webkit.*; +import android.widget.*; +import androidx.appcompat.app.*; +import com.chaquo.python.*; +import java.io.*; + +public class JavaDemoActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_java_demo); + TextView tvCaption = (TextView) findViewById(R.id.tvCaption); + tvCaption.setText(R.string.java_demo_caption); + + WebView wvSource = (WebView) findViewById(R.id.wvSource); + try { + viewSource(this, wvSource, "JavaDemoActivity.java"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static final String ASSET_SOURCE_DIR = "source"; + private static final String EXTRA_CSS = + "body { background-color: #eeeeee; font-size: 85%; }"; + + // Compare with the equivalent Python code in chaquopy/demo/ui_demo.py + private static void viewSource(Context context, WebView wv, + String filename) throws IOException { + InputStream stream = context.getAssets().open + (ASSET_SOURCE_DIR + "/" + filename); + BufferedReader reader = + new BufferedReader(new InputStreamReader(stream)); + String text = ""; + String line; + while ((line = reader.readLine()) != null) { + text += line + "\n"; + } + + Python py = Python.getInstance(); + PyObject pygments = py.getModule("pygments"); + PyObject formatters = py.getModule("pygments.formatters"); + PyObject lexers = py.getModule("pygments.lexers"); + + PyObject formatter = formatters.callAttr("HtmlFormatter"); + PyObject lexer = lexers.callAttr("get_lexer_for_filename", filename); + String body = pygments.callAttr("highlight", text, lexer, formatter) + .toString(); + + String html = String.format( + "<html><head><style>%s\n%s</style></head><body>%s</body></html>", + formatter.callAttr("get_style_defs"), EXTRA_CSS, body); + wv.loadData(Base64.encodeToString(html.getBytes("ASCII"), + Base64.DEFAULT), + "text/html", "base64"); + } +} diff --git a/app/app/src/main/java/com/chaquo/python/demo/JavaTestActivity.java b/app/app/src/main/java/com/chaquo/python/demo/JavaTestActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..fe6dd0ee8ce7d7f1f6ee428eeff89420ba40c7d7 --- /dev/null +++ b/app/app/src/main/java/com/chaquo/python/demo/JavaTestActivity.java @@ -0,0 +1,98 @@ +package com.chaquo.python.demo; + +import android.app.*; +import com.chaquo.python.utils.*; +import java.io.*; +import org.junit.runner.*; +import org.junit.runner.notification.*; + +public class JavaTestActivity extends ConsoleActivity { + + @Override protected Class<? extends JavaTestTask> getTaskClass() { + return JavaTestTask.class; + } + + // ============================================================================================= + + public static class JavaTestTask extends Task { + + public JavaTestTask(Application app) { + super(app); + } + + @Override public void run() { + JUnitCore juc = new JUnitCore(); + juc.addListener(new Listener()); + juc.run(com.chaquo.java.TestSuite.class); + } + + private class Listener extends RunListener { + private ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + private ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + + @Override + public void testRunStarted(Description description) throws Exception { + System.setOut(new PrintStream(stdout)); + System.setErr(new PrintStream(stderr)); + } + + @Override + public void testStarted(Description description) throws Exception { + output(description.toString() + "\n"); + } + + @Override + public void testIgnored(Description description) throws Exception { + output("IGNORED\n"); + } + + @Override + public void testFinished(Description description) throws Exception { + output(stdout.toString()); + stdout.reset(); + outputError(stderr.toString()); + stderr.reset(); + } + + @Override + public void testFailure(Failure failure) throws Exception { + String trace = failure.getTrace(); + StringBuilder filteredTrace = new StringBuilder(); + boolean filterOn = false; + int lineNum = 0; + for (String line : trace.split("\n")) { + lineNum++; + if (lineNum > 1 && line.matches(".*org.junit.*")) { + filterOn = true; + } else { + if (filterOn) { + filteredTrace.append("...\n"); + filterOn = false; + } + filteredTrace.append(line).append("\n"); + } + } + outputError(filteredTrace); + } + + @Override + public void testAssumptionFailure(Failure failure) { + outputError("ASSUMPTION FAILED\n"); + } + + @Override + public void testRunFinished(Result result) throws Exception { + String message = String.format( + "Ran %s tests in %.3f seconds (%s failed, %s ignored)\n", + result.getRunCount(), result.getRunTime() / 1000.0, + result.getFailureCount(), result.getIgnoreCount()); + if (result.getFailureCount() > 0) { + outputError(message); + } else { + output(message); + } + } + } + } + +} diff --git a/app/app/src/main/java/com/chaquo/python/demo/TestAndroidReflect.java b/app/app/src/main/java/com/chaquo/python/demo/TestAndroidReflect.java new file mode 100644 index 0000000000000000000000000000000000000000..c5f4845210be6c7019ba3599608cc9cbb459e19a --- /dev/null +++ b/app/app/src/main/java/com/chaquo/python/demo/TestAndroidReflect.java @@ -0,0 +1,21 @@ +package com.chaquo.python.demo; + +import android.view.textclassifier.*; + +@SuppressWarnings("unused") +public class TestAndroidReflect { + public TextClassifier tcFieldPublic; + protected TextClassifier tcFieldProtected; + + public TextClassifier tcMethodPublic() { return null; } + protected TextClassifier tcMethodProtected() { return null; } + + public int iFieldPublic; + protected int iFieldProtected; + + public int iMethodPublic() { return 0; } + protected int iMethodProtected() { return 0; } + + @Override + protected void finalize() {} +} diff --git a/app/app/src/main/java/com/example/privatestorage/FirstFragment.kt b/app/app/src/main/java/com/example/privatestorage/FirstFragment.kt deleted file mode 100644 index 66b938151831bae65b17537349473d3df9f0eb15..0000000000000000000000000000000000000000 --- a/app/app/src/main/java/com/example/privatestorage/FirstFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.privatestorage - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.navigation.fragment.findNavController -import com.example.privatestorage.databinding.FragmentFirstBinding - -/** - * A simple [Fragment] subclass as the default destination in the navigation. - */ -class FirstFragment : Fragment() { - - private var _binding: FragmentFirstBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - - _binding = FragmentFirstBinding.inflate(inflater, container, false) - return binding.root - - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.buttonFirst.setOnClickListener { - findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/app/src/main/java/com/example/privatestorage/MainActivity.kt b/app/app/src/main/java/com/example/privatestorage/MainActivity.kt deleted file mode 100644 index 96af4747569c11190f939918289dd0125d9f94e0..0000000000000000000000000000000000000000 --- a/app/app/src/main/java/com/example/privatestorage/MainActivity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.privatestorage - -import android.os.Bundle -import com.google.android.material.snackbar.Snackbar -import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp -import androidx.navigation.ui.setupActionBarWithNavController -import android.view.Menu -import android.view.MenuItem -import com.example.privatestorage.databinding.ActivityMainBinding - -class MainActivity : AppCompatActivity() { - - private lateinit var appBarConfiguration: AppBarConfiguration - private lateinit var binding: ActivityMainBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - setSupportActionBar(binding.toolbar) - - val navController = findNavController(R.id.nav_host_fragment_content_main) - appBarConfiguration = AppBarConfiguration(navController.graph) - setupActionBarWithNavController(navController, appBarConfiguration) - - binding.fab.setOnClickListener { view -> - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) - .setAction("Action", null).show() - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - // Inflate the menu; this adds items to the action bar if it is present. - menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - return when (item.itemId) { - R.id.action_settings -> true - else -> super.onOptionsItemSelected(item) - } - } - - override fun onSupportNavigateUp(): Boolean { - val navController = findNavController(R.id.nav_host_fragment_content_main) - return navController.navigateUp(appBarConfiguration) - || super.onSupportNavigateUp() - } -} \ No newline at end of file diff --git a/app/app/src/main/java/com/example/privatestorage/SecondFragment.kt b/app/app/src/main/java/com/example/privatestorage/SecondFragment.kt deleted file mode 100644 index ff498cc7bef7d1ccc3bb663e00b5dd82649602a5..0000000000000000000000000000000000000000 --- a/app/app/src/main/java/com/example/privatestorage/SecondFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.privatestorage - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.navigation.fragment.findNavController -import com.example.privatestorage.databinding.FragmentSecondBinding - -/** - * A simple [Fragment] subclass as the second destination in the navigation. - */ -class SecondFragment : Fragment() { - - private var _binding: FragmentSecondBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - - _binding = FragmentSecondBinding.inflate(inflater, container, false) - return binding.root - - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.buttonSecond.setOnClickListener { - findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/app/src/main/python/chaquopy/__init__.py b/app/app/src/main/python/chaquopy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/app/src/main/python/chaquopy/demo/__init__.py b/app/app/src/main/python/chaquopy/demo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/app/src/main/python/chaquopy/demo/ui_demo.py b/app/app/src/main/python/chaquopy/demo/ui_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..a9910ea50e17b9189a69872b43c321c8b7c50a09 --- /dev/null +++ b/app/app/src/main/python/chaquopy/demo/ui_demo.py @@ -0,0 +1,136 @@ +from java import dynamic_proxy, jboolean, jvoid, Override, static_proxy + +from android.app import AlertDialog +from android.content import Context, DialogInterface +from android.graphics.drawable import ColorDrawable +from android.os import Bundle +from androidx.appcompat.app import AppCompatActivity +from androidx.fragment.app import DialogFragment +from androidx.preference import Preference, PreferenceFragmentCompat +from android.view import Menu, MenuItem, View +from java.lang import String + +from com.chaquo.python.demo import R + + +class UIDemoActivity(static_proxy(AppCompatActivity)): + @Override(jvoid, [Bundle]) + def onCreate(self, state): + AppCompatActivity.onCreate(self, state) + if state is None: + state = Bundle() + self.setContentView(R.layout.activity_menu) + self.findViewById(R.id.tvCaption).setText(R.string.demo_caption) + + self.title_drawable = ColorDrawable() + self.getSupportActionBar().setBackgroundDrawable(self.title_drawable) + self.title_drawable.setColor( + state.getInt("title_color", self.getResources().getColor(R.color.blue))) + + self.wvSource = self.findViewById(R.id.wvSource) + view_source(self, self.wvSource, "ui_demo.py") + self.wvSource.setVisibility(state.getInt("source_visibility", View.GONE)) + + self.getSupportFragmentManager().beginTransaction()\ + .replace(R.id.flMenu, MenuFragment()).commit() + + @Override(jvoid, [Bundle]) + def onSaveInstanceState(self, state): + state.putInt("source_visibility", self.wvSource.getVisibility()) + state.putInt("title_color", self.title_drawable.getColor()) + + @Override(jboolean, [Menu]) + def onCreateOptionsMenu(self, menu): + self.getMenuInflater().inflate(R.menu.view_source, menu) + return True + + @Override(jboolean, [MenuItem]) + def onOptionsItemSelected(self, item): + id = item.getItemId() + if id == R.id.menu_source: + vis = self.wvSource.getVisibility() + new_vis = View.VISIBLE if (vis == View.GONE) else View.GONE + self.wvSource.setVisibility(new_vis) + return True + else: + return False + + +class MenuFragment(static_proxy(PreferenceFragmentCompat)): + @Override(jvoid, [Bundle, String]) + def onCreatePreferences(self, state, rootKey): + self.addPreferencesFromResource(R.xml.activity_ui_demo) + + from android.media import AudioManager, SoundPool + self.sound_pool = SoundPool(1, AudioManager.STREAM_MUSIC, 0) + self.sound_id = self.sound_pool.load(self.getActivity(), R.raw.sound, 1) + + @Override(jboolean, [Preference]) + def onPreferenceTreeClick(self, pref): + method = getattr(self, pref.getKey()) + if method: + method(self.getActivity()) + return True + else: + return False + + def demo_dialog(self, activity): + ColorDialog().show(self.getFragmentManager(), "color") + + def demo_toast(self, activity): + from android.widget import Toast + Toast.makeText(activity, R.string.demo_toast_text, + Toast.LENGTH_SHORT).show() + + def demo_sound(self, activity): + self.sound_pool.play(self.sound_id, 1, 1, 0, 0, 1) + + def demo_vibrate(self, activity): + activity.getSystemService(Context.VIBRATOR_SERVICE)\ + .vibrate(200) + + +class ColorDialog(static_proxy(DialogFragment)): + @Override(AlertDialog, [Bundle]) + def onCreateDialog(self, state): + activity = self.getActivity() + builder = AlertDialog.Builder(activity) + builder.setTitle(R.string.demo_dialog_title) + builder.setMessage(R.string.demo_dialog_text) + + class Listener(dynamic_proxy(DialogInterface.OnClickListener)): + def __init__(self, color_res): + super(Listener, self).__init__() + self.color = activity.getResources().getColor(color_res) + + def onClick(self, dialog, which): + activity.title_drawable.setColor(self.color) + + builder.setNegativeButton(R.string.red, Listener(R.color.red)) + builder.setNeutralButton(R.string.green, Listener(R.color.green)) + builder.setPositiveButton(R.string.blue, Listener(R.color.blue)) + return builder.create() + + +ASSET_SOURCE_DIR = "source" +EXTRA_CSS = "body { background-color: #eeeeee; font-size: 85%; }" + +# Compare with the equivalent Java code in JavaDemoActivity.java +def view_source(context, web_view, filename): + from base64 import b64encode + from os.path import join + from pygments import highlight + from pygments.formatters import HtmlFormatter + from pygments.lexers import get_lexer_for_filename + + from java.io import BufferedReader, InputStreamReader + + stream = context.getAssets().open(join(ASSET_SOURCE_DIR, filename)) + reader = BufferedReader(InputStreamReader(stream)) + text = "\n".join(iter(reader.readLine, None)) + + formatter = HtmlFormatter() + body = highlight(text, get_lexer_for_filename(filename), formatter) + html = ("<html><head><style>{}\n{}</style></head><body>{}</body></html>" + .format(formatter.get_style_defs(), EXTRA_CSS, body)).encode() + web_view.loadData(b64encode(html).decode(), "text/html", "base64") diff --git a/app/app/src/main/python/main.py b/app/app/src/main/python/main.py deleted file mode 100644 index 30d202f5caad9b53666bda5e0cfd4c8e92f39ffa..0000000000000000000000000000000000000000 --- a/app/app/src/main/python/main.py +++ /dev/null @@ -1,14 +0,0 @@ -from kivy.app import App -from kivy.uix.label import Label - -class MainApp(App): - def build(self): - label = Label(text='Hello from Kivy', - size_hint=(.5, .5), - pos_hint={'center_x': .5, 'center_y': .5}) - - return label - -if __name__ == '__main__': - app = MainApp() - app.run() diff --git a/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d11462a4b96669193de13a711a3a36220a0..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt" - android:width="108dp" - android:height="108dp" - android:viewportWidth="108" - android:viewportHeight="108"> - <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> - <aapt:attr name="android:fillColor"> - <gradient - android:endX="85.84757" - android:endY="92.4963" - android:startX="42.9492" - android:startY="49.59793" - android:type="linear"> - <item - android:color="#44000000" - android:offset="0.0" /> - <item - android:color="#00000000" - android:offset="1.0" /> - </gradient> - </aapt:attr> - </path> - <path - android:fillColor="#FFFFFF" - android:fillType="nonZero" - android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" - android:strokeWidth="1" - android:strokeColor="#00000000" /> -</vector> \ No newline at end of file diff --git a/app/app/src/main/res/drawable/ic_launcher_background.xml b/app/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cbf141911847041df5d7b87f0dd5ef9d4..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="108dp" - android:height="108dp" - android:viewportWidth="108" - android:viewportHeight="108"> - <path - android:fillColor="#3DDC84" - android:pathData="M0,0h108v108h-108z" /> - <path - android:fillColor="#00000000" - android:pathData="M9,0L9,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,0L19,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M29,0L29,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M39,0L39,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M49,0L49,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M59,0L59,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M69,0L69,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M79,0L79,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M89,0L89,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M99,0L99,108" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,9L108,9" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,19L108,19" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,29L108,29" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,39L108,39" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,49L108,49" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,59L108,59" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,69L108,69" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,79L108,79" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,89L108,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M0,99L108,99" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,29L89,29" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,39L89,39" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,49L89,49" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,59L89,59" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,69L89,69" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M19,79L89,79" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M29,19L29,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M39,19L39,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M49,19L49,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M59,19L59,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M69,19L69,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> - <path - android:fillColor="#00000000" - android:pathData="M79,19L79,89" - android:strokeWidth="0.8" - android:strokeColor="#33FFFFFF" /> -</vector> diff --git a/app/app/src/main/res/layout/activity_java_demo.xml b/app/app/src/main/res/layout/activity_java_demo.xml new file mode 100644 index 0000000000000000000000000000000000000000..303916bd6ae268d7203974e8dd82c0d6204ec418 --- /dev/null +++ b/app/app/src/main/res/layout/activity_java_demo.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.chaquo.python.demo.JavaDemoActivity"> + + <TextView + android:id="@+id/tvCaption" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintHorizontal_bias="0.0"/> + + <WebView + android:id="@+id/wvSource" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="0dp" + android:layout_marginEnd="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:visibility="visible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tvCaption" + app:layout_constraintVertical_bias="0.0"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/app/src/main/res/layout/activity_main.xml b/app/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 11daef10356ea126f78bef34b21eb396f93983f6..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".MainActivity"> - - <com.google.android.material.appbar.AppBarLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:theme="@style/Theme.PrivateStorage.AppBarOverlay"> - - <androidx.appcompat.widget.Toolbar - android:id="@+id/toolbar" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - android:background="?attr/colorPrimary" - app:popupTheme="@style/Theme.PrivateStorage.PopupOverlay" /> - - </com.google.android.material.appbar.AppBarLayout> - - <include layout="@layout/content_main" /> - - <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/fab" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="bottom|end" - android:layout_marginEnd="@dimen/fab_margin" - android:layout_marginBottom="16dp" - app:srcCompat="@android:drawable/ic_dialog_email" /> - -</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/app/src/main/res/layout/content_main.xml b/app/app/src/main/res/layout/content_main.xml deleted file mode 100644 index e416e1c18d53ed5397bc0e696dcec55833cc91e5..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - <fragment - android:id="@+id/nav_host_fragment_content_main" - android:name="androidx.navigation.fragment.NavHostFragment" - android:layout_width="0dp" - android:layout_height="0dp" - app:defaultNavHost="true" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:navGraph="@navigation/nav_graph" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/app/src/main/res/layout/fragment_first.xml b/app/app/src/main/res/layout/fragment_first.xml deleted file mode 100644 index fb44a3d9176c7d931719255605c270c686734e3e..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/layout/fragment_first.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".FirstFragment"> - - <TextView - android:id="@+id/textview_first" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/hello_first_fragment" - app:layout_constraintBottom_toTopOf="@id/button_first" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <Button - android:id="@+id/button_first" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/next" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/textview_first" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/app/src/main/res/layout/fragment_second.xml b/app/app/src/main/res/layout/fragment_second.xml deleted file mode 100644 index bd9052422ed284cd786132c85decbb447d84a7e5..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/layout/fragment_second.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".SecondFragment"> - - <TextView - android:id="@+id/textview_second" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:layout_constraintBottom_toTopOf="@id/button_second" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <Button - android:id="@+id/button_second" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/previous" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/textview_second" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/app/src/main/res/menu/menu_main.xml b/app/app/src/main/res/menu/menu_main.xml deleted file mode 100644 index 33462d365a175a690910dc9afedb7389a39ea810..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/menu/menu_main.xml +++ /dev/null @@ -1,10 +0,0 @@ -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - tools:context="com.example.privatestorage.MainActivity"> - <item - android:id="@+id/action_settings" - android:orderInCategory="100" - android:title="@string/action_settings" - app:showAsAction="never" /> -</menu> \ No newline at end of file diff --git a/app/app/src/main/res/menu/view_source.xml b/app/app/src/main/res/menu/view_source.xml new file mode 100644 index 0000000000000000000000000000000000000000..1e34fee847f7dfd9be56b54586e6ea40554bc407 --- /dev/null +++ b/app/app/src/main/res/menu/view_source.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item android:id="@+id/menu_source" + android:title="@string/view_source" + app:showAsAction="always|withText"/> + +</menu> \ No newline at end of file diff --git a/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cfe52eac1ba66ba280a68ca7be8fcf88a16..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@drawable/ic_launcher_background" /> - <foreground android:drawable="@drawable/ic_launcher_foreground" /> -</adaptive-icon> \ No newline at end of file diff --git a/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cfe52eac1ba66ba280a68ca7be8fcf88a16..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@drawable/ic_launcher_background" /> - <foreground android:drawable="@drawable/ic_launcher_foreground" /> -</adaptive-icon> \ No newline at end of file diff --git a/app/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da081676d42f6c3f78a2c91e7bcedddedb..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9f036a47549d47db79c16788749dca10..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f5083623b375139afb391af71cc533a7dd37..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e6fa1074b79ccd52ef67ac15c5637e85..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37cbc3587421d6889eadd1d91fbf1994d4..0000000000000000000000000000000000000000 Binary files a/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/app/src/main/res/navigation/nav_graph.xml b/app/app/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index b698442f5fa8c0ccf12fa668b2d3efdeaf2b01ce..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<navigation xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/nav_graph" - app:startDestination="@id/FirstFragment"> - - <fragment - android:id="@+id/FirstFragment" - android:name="com.example.privatestorage.FirstFragment" - android:label="@string/first_fragment_label" - tools:layout="@layout/fragment_first"> - - <action - android:id="@+id/action_FirstFragment_to_SecondFragment" - app:destination="@id/SecondFragment" /> - </fragment> - <fragment - android:id="@+id/SecondFragment" - android:name="com.example.privatestorage.SecondFragment" - android:label="@string/second_fragment_label" - tools:layout="@layout/fragment_second"> - - <action - android:id="@+id/action_SecondFragment_to_FirstFragment" - app:destination="@id/FirstFragment" /> - </fragment> -</navigation> \ No newline at end of file diff --git a/app/app/src/main/res/raw/sound.ogg b/app/app/src/main/res/raw/sound.ogg new file mode 100644 index 0000000000000000000000000000000000000000..c9b030f06b1e306700d2269c9313f229eeac4cfe Binary files /dev/null and b/app/app/src/main/res/raw/sound.ogg differ diff --git a/app/app/src/main/res/values-land/dimens.xml b/app/app/src/main/res/values-land/dimens.xml deleted file mode 100644 index 22d7f004329e4ff0529a55f37011a5ade468a19b..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/values-land/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ -<resources> - <dimen name="fab_margin">48dp</dimen> -</resources> \ No newline at end of file diff --git a/app/app/src/main/res/values-night/themes.xml b/app/app/src/main/res/values-night/themes.xml deleted file mode 100644 index f06919cf0182145ab69bdd4f6ad47f5e357013a1..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ -<resources xmlns:tools="http://schemas.android.com/tools"> - <!-- Base application theme. --> - <style name="Theme.PrivateStorage" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> - <!-- Primary brand color. --> - <item name="colorPrimary">@color/purple_200</item> - <item name="colorPrimaryVariant">@color/purple_700</item> - <item name="colorOnPrimary">@color/black</item> - <!-- Secondary brand color. --> - <item name="colorSecondary">@color/teal_200</item> - <item name="colorSecondaryVariant">@color/teal_200</item> - <item name="colorOnSecondary">@color/black</item> - <!-- Status bar color. --> - <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> - <!-- Customize your theme here. --> - </style> -</resources> \ No newline at end of file diff --git a/app/app/src/main/res/values-w1240dp/dimens.xml b/app/app/src/main/res/values-w1240dp/dimens.xml deleted file mode 100644 index d73f4a359b5bbcb8fc1b5508036cee64621c0fc3..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/values-w1240dp/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ -<resources> - <dimen name="fab_margin">200dp</dimen> -</resources> \ No newline at end of file diff --git a/app/app/src/main/res/values-w600dp/dimens.xml b/app/app/src/main/res/values-w600dp/dimens.xml deleted file mode 100644 index 22d7f004329e4ff0529a55f37011a5ade468a19b..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/values-w600dp/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ -<resources> - <dimen name="fab_margin">48dp</dimen> -</resources> \ No newline at end of file diff --git a/app/app/src/main/res/values/colors.xml b/app/app/src/main/res/values/colors.xml index f8c6127d327620c93d2b2d00342a68e97b98a48d..2d0885c0cdc86f6959f08ef17ee80f84b31d25da 100644 --- a/app/app/src/main/res/values/colors.xml +++ b/app/app/src/main/res/values/colors.xml @@ -1,10 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="purple_200">#FFBB86FC</color> - <color name="purple_500">#FF6200EE</color> - <color name="purple_700">#FF3700B3</color> - <color name="teal_200">#FF03DAC5</color> - <color name="teal_700">#FF018786</color> - <color name="black">#FF000000</color> - <color name="white">#FFFFFFFF</color> -</resources> \ No newline at end of file + <color name="red">#b53f3f</color> + <color name="green">#3f8e3d</color> + <color name="blue">@color/colorPrimary</color> +</resources> diff --git a/app/app/src/main/res/values/dimens.xml b/app/app/src/main/res/values/dimens.xml deleted file mode 100644 index 125df8711919075db4015d1ec937aff0377904bb..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ -<resources> - <dimen name="fab_margin">16dp</dimen> -</resources> \ No newline at end of file diff --git a/app/app/src/main/res/values/strings.xml b/app/app/src/main/res/values/strings.xml index 0fdfc53e7d1431ce937eda591f1d9bc6854eda98..6d00cacad6bdf14fe8b3002d71714eb18293ff99 100644 --- a/app/app/src/main/res/values/strings.xml +++ b/app/app/src/main/res/values/strings.xml @@ -1,12 +1,33 @@ <resources> - <string name="app_name">PrivateStorage</string> - <string name="action_settings">Settings</string> - <!-- Strings used for fragments for navigation --> - <string name="first_fragment_label">First Fragment</string> - <string name="second_fragment_label">Second Fragment</string> - <string name="next">Next</string> - <string name="previous">Previous</string> - - <string name="hello_first_fragment">Hello first fragment</string> - <string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string> -</resources> \ No newline at end of file + + <string name="app_id">com.chaquo.python.demo3</string> + <string name="app_name">Chaquopy demo</string> + + <string name="ui_demo">Android UI demo</string> + <string name="ui_demo_summary">Demonstrates writing an app entirely in Python</string> + <string name="demo_caption">This activity was written entirely in Python using the Chaquopy + Python API. To view its source code, press the button above.</string> + <string name="view_source">View source</string> + <string name="demo_dialog">Dialog box</string> + <string name="demo_dialog_title">Dialog from Python</string> + <string name="demo_dialog_text">Select title color</string> + <string name="red">Red</string> + <string name="green">Green</string> + <string name="blue">Blue</string> + <string name="demo_notify">Notification</string> + <string name="demo_notify_title">Notification from Python</string> + <string name="demo_notify_text">Lorem ipsum dolor sit amet</string> + <string name="demo_toast">Toast</string> + <string name="demo_toast_text">Toast from Python</string> + <string name="demo_sound">Sound</string> + <string name="demo_vibrate">Vibration</string> + + <string name="java_demo">Java API demo</string> + <string name="java_demo_summary">Demonstrates using a Python library from Java code</string> + <string name="java_demo_caption">This activity uses the Chaquopy Java API and the Python + Pygments library to format a copy of its own source code as HTML.</string> + + <string name="java_unit_test">Java unit tests</string> + <string name="python_unit_test">Python unit tests</string> + +</resources> diff --git a/app/app/src/main/res/values/themes.xml b/app/app/src/main/res/values/themes.xml deleted file mode 100644 index 04b38cabcaec9c570900df8d69273d1bb8fbc67e..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,25 +0,0 @@ -<resources xmlns:tools="http://schemas.android.com/tools"> - <!-- Base application theme. --> - <style name="Theme.PrivateStorage" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> - <!-- Primary brand color. --> - <item name="colorPrimary">@color/purple_500</item> - <item name="colorPrimaryVariant">@color/purple_700</item> - <item name="colorOnPrimary">@color/white</item> - <!-- Secondary brand color. --> - <item name="colorSecondary">@color/teal_200</item> - <item name="colorSecondaryVariant">@color/teal_700</item> - <item name="colorOnSecondary">@color/black</item> - <!-- Status bar color. --> - <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> - <!-- Customize your theme here. --> - </style> - - <style name="Theme.PrivateStorage.NoActionBar"> - <item name="windowActionBar">false</item> - <item name="windowNoTitle">true</item> - </style> - - <style name="Theme.PrivateStorage.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> - - <style name="Theme.PrivateStorage.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> -</resources> \ No newline at end of file diff --git a/app/app/src/main/res/xml/activity_main.xml b/app/app/src/main/res/xml/activity_main.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d586b169f912f30c841495ea168cab4d5c2ec03 --- /dev/null +++ b/app/app/src/main/res/xml/activity_main.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + android:orderingFromXml="true"> + + <Preference android:title="@string/repl" android:summary="@string/repl_summary"> + <intent android:targetPackage="@string/app_id" + android:targetClass="com.chaquo.python.utils.ReplActivity"/> + </Preference> + + <Preference android:title="@string/ui_demo" android:summary="@string/ui_demo_summary"> + <intent android:targetPackage="@string/app_id" + android:targetClass="chaquopy.demo.ui_demo.UIDemoActivity"/> + </Preference> + + <Preference android:title="@string/java_demo" android:summary="@string/java_demo_summary"> + <intent android:targetPackage="@string/app_id" + android:targetClass="com.chaquo.python.demo.JavaDemoActivity"/> + </Preference> + + <Preference android:title="@string/java_unit_test"> + <intent android:targetPackage="@string/app_id" + android:targetClass="com.chaquo.python.demo.JavaTestActivity"/> + </Preference> + + <Preference android:title="@string/python_unit_test"> + <intent android:targetPackage="@string/app_id" + android:targetClass="com.chaquo.python.utils.PythonTestActivity"/> + </Preference> + +</PreferenceScreen> \ No newline at end of file diff --git a/app/app/src/main/res/xml/activity_ui_demo.xml b/app/app/src/main/res/xml/activity_ui_demo.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b3fcff66fba6e035a23867f83c364a19d1f09f8 --- /dev/null +++ b/app/app/src/main/res/xml/activity_ui_demo.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + android:orderingFromXml="true"> + + <Preference android:title="@string/demo_dialog" android:key="demo_dialog"/> + <Preference android:title="@string/demo_toast" android:key="demo_toast"/> + <Preference android:title="@string/demo_sound" android:key="demo_sound"/> + <Preference android:title="@string/demo_vibrate" android:key="demo_vibrate"/> + +</PreferenceScreen> \ No newline at end of file diff --git a/app/app/src/main/res/xml/backup_rules.xml b/app/app/src/main/res/xml/backup_rules.xml deleted file mode 100644 index fa0f996d2c2a6bdd11f5371de4268c8389d6c720..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/xml/backup_rules.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - Sample backup rules file; uncomment and customize as necessary. - See https://developer.android.com/guide/topics/data/autobackup - for details. - Note: This file is ignored for devices older that API 31 - See https://developer.android.com/about/versions/12/backup-restore ---> -<full-backup-content> - <!-- - <include domain="sharedpref" path="."/> - <exclude domain="sharedpref" path="device.xml"/> ---> -</full-backup-content> \ No newline at end of file diff --git a/app/app/src/main/res/xml/data_extraction_rules.xml b/app/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index 9ee9997b0b4726e57c27b2f7b21462b604ff8a88..0000000000000000000000000000000000000000 --- a/app/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - Sample data extraction rules file; uncomment and customize as necessary. - See https://developer.android.com/about/versions/12/backup-restore#xml-changes - for details. ---> -<data-extraction-rules> - <cloud-backup> - <!-- TODO: Use <include> and <exclude> to control what is backed up. - <include .../> - <exclude .../> - --> - </cloud-backup> - <!-- - <device-transfer> - <include .../> - <exclude .../> - </device-transfer> - --> -</data-extraction-rules> \ No newline at end of file diff --git a/app/app/src/test/java/com/chaquo/python/utils/BufferedLiveEventTest.java b/app/app/src/test/java/com/chaquo/python/utils/BufferedLiveEventTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7156b52dca1d068466fc8e0cf3314f254c3c9309 --- /dev/null +++ b/app/app/src/test/java/com/chaquo/python/utils/BufferedLiveEventTest.java @@ -0,0 +1,91 @@ +package com.chaquo.python.utils; + +import androidx.annotation.*; +import androidx.arch.core.executor.testing.*; +import androidx.lifecycle.*; +import androidx.lifecycle.Observer; +import java.util.*; +import org.junit.*; +import org.junit.rules.*; + +import static androidx.lifecycle.Lifecycle.Event.*; +import static org.junit.Assert.*; + +public class BufferedLiveEventTest implements LifecycleOwner { + + @Rule public TestRule instantExecutorRule = new InstantTaskExecutorRule(); + @Rule public ExpectedException thrown = ExpectedException.none(); + + private LifecycleRegistry lifecycle = new LifecycleRegistry(this); + + private BufferedLiveEvent<String> ble = new BufferedLiveEvent<>(); + + private static class MockObserver<T> implements Observer<T> { + public List<T> observed = new ArrayList<>(); + + @Override public void onChanged(@Nullable T t) { + observed.add(t); + } + } + + private MockObserver<String> observer = new MockObserver<>(); + + @Before public void setUp() throws Exception { + ble.observe(this, observer); + assertObserved(); + } + + @NonNull @Override public Lifecycle getLifecycle() { + return lifecycle; + } + + @Test public void startStopStart() { + ble.setValue("a"); + assertObserved(); + lifecycle.handleLifecycleEvent(ON_START); + assertObserved("a"); + ble.setValue("b"); + assertObserved("b"); + ble.setValue("c"); + lifecycle.handleLifecycleEvent(ON_STOP); + ble.setValue("d"); + ble.setValue("e"); + assertObserved("c"); + lifecycle.handleLifecycleEvent(ON_START); + assertObserved("d", "e"); + } + + @Test public void nullValue() { + lifecycle.handleLifecycleEvent(ON_START); + ble.setValue("a"); + ble.setValue(null); + ble.setValue("b"); + assertObserved("a", null, "b"); + } + + @Test public void observeMultiple() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Cannot register multiple observers on a SingleLiveEvent"); + ble.observe(this, new Observer<String>() { + @Override public void onChanged(@Nullable String s) {} + }); + } + + @Test public void observeRemoveObserve() { + lifecycle.handleLifecycleEvent(ON_START); + ble.setValue("a"); + assertObserved("a"); + ble.removeObserver(observer); + ble.setValue("c"); + ble.setValue("d"); + assertObserved(); + ble.observe(this, observer); + assertObserved("c", "d"); + } + + public void assertObserved(String... expected) { + assertEquals(Arrays.asList(expected), observer.observed); + observer.observed.clear(); + } + +} diff --git a/app/app/src/test/java/com/example/privatestorage/ExampleUnitTest.kt b/app/app/src/test/java/com/example/privatestorage/ExampleUnitTest.kt deleted file mode 100644 index 171bd4c6c16021ef01c41b61077a26d4fa74c894..0000000000000000000000000000000000000000 --- a/app/app/src/test/java/com/example/privatestorage/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.privatestorage - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/app/app/src/utils/java/com/chaquo/python/utils/App.java b/app/app/src/utils/java/com/chaquo/python/utils/App.java new file mode 100644 index 0000000000000000000000000000000000000000..6526a42190537665f92dc805082705659f53c2b7 --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/App.java @@ -0,0 +1,20 @@ +package com.chaquo.python.utils; + +import android.content.*; +import androidx.preference.*; +import com.chaquo.python.android.*; + + +public class App extends PyApplication { + + public static App context; + public static SharedPreferences prefs; + + @Override + public void onCreate() { + super.onCreate(); + context = this; + prefs = PreferenceManager.getDefaultSharedPreferences(this); + } + +} \ No newline at end of file diff --git a/app/app/src/utils/java/com/chaquo/python/utils/BufferedLiveEvent.java b/app/app/src/utils/java/com/chaquo/python/utils/BufferedLiveEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..573e99dc8718609e0c755ddae773f6d015b8d18c --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/BufferedLiveEvent.java @@ -0,0 +1,45 @@ +package com.chaquo.python.utils; + +import android.os.*; +import androidx.annotation.*; +import java.util.*; + +/** Similar to SimpleLiveEvent, but any values set while inactive will be buffered. As soon as + * we have an active observer, it will be notified of those values in the same order as they were + * set. */ +public class BufferedLiveEvent<T> extends SingleLiveEvent<T> { + + private ArrayList<T> mBuffer = new ArrayList<>(); + private Handler mHandler; + + /** Unlike in the base class, multiple calls to postData will always result in multiple values + * being notified to the observer. */ + @Override public void postValue(@Nullable final T value) { + // Delay initialization for unit tests. + if (mHandler == null) { + mHandler = new Handler(Looper.getMainLooper()); + } + mHandler.post(new Runnable() { + @Override public void run() { + setValue(value); + } + }); + } + + @Override public void setValue(@Nullable T t) { + if (hasActiveObservers() && mBuffer.isEmpty()) { // See onActive + super.setValue(t); + } else { + mBuffer.add(t); + } + } + + @Override protected void onActive() { + // Don't use a foreach loop, an observer might call setValue and lengthen the buffer. + for (int i = 0; i < mBuffer.size(); i++) { + super.setValue(mBuffer.get(i)); + } + mBuffer.clear(); + } + +} diff --git a/app/app/src/utils/java/com/chaquo/python/utils/ConsoleActivity.java b/app/app/src/utils/java/com/chaquo/python/utils/ConsoleActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..a07807c623bae96ba2e1a4c698f1021289c0893b --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/ConsoleActivity.java @@ -0,0 +1,398 @@ +package com.chaquo.python.utils; + +import androidx.annotation.NonNull; +import android.app.*; +import android.graphics.*; +import android.os.*; +import android.text.*; +import android.text.style.*; +import android.view.*; +import android.view.inputmethod.*; +import android.widget.*; +import androidx.annotation.*; +import androidx.appcompat.app.*; +import androidx.core.content.*; +import androidx.lifecycle.*; + +public abstract class ConsoleActivity extends AppCompatActivity +implements ViewTreeObserver.OnGlobalLayoutListener, ViewTreeObserver.OnScrollChangedListener { + + // Because tvOutput has freezesText enabled, letting it get too large can cause a + // TransactionTooLargeException. The limit isn't in the saved state itself, but in the + // Binder transaction which transfers it to the system server. So it doesn't happen if + // you're rotating the screen, but it does happen when you press Back. + // + // The exception message shows the size of the failed transaction, so I can determine from + // experiment that the limit is about 500 KB, and each character consumes 4 bytes. + private final int MAX_SCROLLBACK_LEN = 100000; + + private EditText etInput; + private ScrollView svOutput; + private TextView tvOutput; + private int outputWidth = -1, outputHeight = -1; + + enum Scroll { + TOP, BOTTOM + } + private Scroll scrollRequest; + + public static class ConsoleModel extends ViewModel { + boolean pendingNewline = false; // Prevent empty line at bottom of screen + int scrollChar = 0; // Character offset of the top visible line. + int scrollAdjust = 0; // Pixels by which that line is scrolled above the top + // (prevents movement when keyboard hidden/shown). + } + private ConsoleModel consoleModel; + + protected Task task; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + consoleModel = ViewModelProviders.of(this).get(ConsoleModel.class); + task = ViewModelProviders.of(this).get(getTaskClass()); + setContentView(resId("layout", "activity_console")); + createInput(); + createOutput(); + } + + protected abstract Class<? extends Task> getTaskClass(); + + private void createInput() { + etInput = findViewById(resId("id", "etInput")); + + // Strip formatting from pasted text. + etInput.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + public void onTextChanged(CharSequence s, int start, int before, int count) {} + public void afterTextChanged(Editable e) { + for (CharacterStyle cs : e.getSpans(0, e.length(), CharacterStyle.class)) { + e.removeSpan(cs); + } + } + }); + + // At least on API level 28, if an ACTION_UP is lost during a rotation, then the app + // (or any other app which takes focus) will receive an endless stream of ACTION_DOWNs + // until the key is pressed again. So we react to ACTION_UP instead. + etInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE || + (event != null && event.getAction() == KeyEvent.ACTION_UP)) { + String text = etInput.getText().toString() + "\n"; + etInput.setText(""); + output(span(text, new StyleSpan(Typeface.BOLD))); + scrollTo(Scroll.BOTTOM); + task.onInput(text); + } + + // If we return false on ACTION_DOWN, we won't be given the ACTION_UP. + return true; + } + }); + + task.inputEnabled.observe(this, new Observer<Boolean>() { + @Override public void onChanged(@Nullable Boolean enabled) { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + if (enabled) { + etInput.setVisibility(View.VISIBLE); + etInput.setEnabled(true); + + // requestFocus alone doesn't always bring up the soft keyboard during startup + // on the Nexus 4 with API level 22: probably some race condition. (After + // rotation with input *already* enabled, the focus may be overridden by + // onRestoreInstanceState, which will run after this observer.) + etInput.requestFocus(); + imm.showSoftInput(etInput, InputMethodManager.SHOW_IMPLICIT); + } else { + // Disable rather than hide, otherwise tvOutput gets a gray background on API + // level 26, like tvCaption in the main menu when you press an arrow key. + etInput.setEnabled(false); + imm.hideSoftInputFromWindow(tvOutput.getWindowToken(), 0); + } + } + }); + } + + private void createOutput() { + svOutput = findViewById(resId("id", "svOutput")); + svOutput.getViewTreeObserver().addOnGlobalLayoutListener(this); + + tvOutput = findViewById(resId("id", "tvOutput")); + if (Build.VERSION.SDK_INT >= 23) { + // noinspection WrongConstant + tvOutput.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE); + } + // Don't start observing task.output yet: we need to restore the scroll position first so + // we maintain the scrolled-to-bottom state. + } + + @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + // Don't restore the UI state unless we have the non-UI state as well. + if (task.getState() != Thread.State.NEW) { + super.onRestoreInstanceState(savedInstanceState); + } + } + + @Override protected void onResume() { + super.onResume(); + // Needs to be in onResume rather than onStart because onRestoreInstanceState runs + // between them. + if (task.getState() == Thread.State.NEW) { + task.start(); + } + } + + @Override protected void onPause() { + super.onPause(); + saveScroll(); // Necessary to save bottom position in case we've never scrolled. + } + + // This callback is run after onResume, after each layout pass. If a view's size, position + // or visibility has changed, the new values will be visible here. + @Override public void onGlobalLayout() { + if (outputWidth != svOutput.getWidth() || outputHeight != svOutput.getHeight()) { + // Can't register this listener in onCreate on API level 15 + // (https://stackoverflow.com/a/35054919). + if (outputWidth == -1) { + svOutput.getViewTreeObserver().addOnScrollChangedListener(this); + } + + // Either we've just started up, or the keyboard has been hidden or shown. + outputWidth = svOutput.getWidth(); + outputHeight = svOutput.getHeight(); + restoreScroll(); + } else if (scrollRequest != null) { + int y = -1; + switch (scrollRequest) { + case TOP: + y = 0; + break; + case BOTTOM: + y = tvOutput.getHeight(); + break; + } + + // Don't use smooth scroll, because if an output call happens while it's animating + // towards the bottom, isScrolledToBottom will believe we've left the bottom and + // auto-scrolling will stop. Don't use fullScroll either, because not only does it use + // smooth scroll, it also grabs focus. + svOutput.scrollTo(0, y); + scrollRequest = null; + } + } + + @Override public void onScrollChanged() { + saveScroll(); + } + + // After a rotation, a ScrollView will restore the previous pixel scroll position. However, due + // to re-wrapping, this may result in a completely different piece of text being visible. We'll + // try to maintain the text position of the top line, unless the view is scrolled to the bottom, + // in which case we'll maintain that. Maintaining the bottom line will also cause a scroll + // adjustment when the keyboard's hidden or shown. + private void saveScroll() { + if (isScrolledToBottom()) { + consoleModel.scrollChar = tvOutput.getText().length(); + consoleModel.scrollAdjust = 0; + } else { + int scrollY = svOutput.getScrollY(); + Layout layout = tvOutput.getLayout(); + if (layout != null) { // See note in restoreScroll + int line = layout.getLineForVertical(scrollY); + consoleModel.scrollChar = layout.getLineStart(line); + consoleModel.scrollAdjust = scrollY - layout.getLineTop(line); + } + } + } + + private void restoreScroll() { + removeCursor(); + + // getLayout sometimes returns null even when called from onGlobalLayout. The + // documentation says this can happen if the "text or width has recently changed", but + // does not define "recently". See Electron Cash issues #1330 and #1592. + Layout layout = tvOutput.getLayout(); + if (layout != null) { + int line = layout.getLineForOffset(consoleModel.scrollChar); + svOutput.scrollTo(0, layout.getLineTop(line) + consoleModel.scrollAdjust); + } + + // If we are now scrolled to the bottom, we should stick there. (scrollTo probably won't + // trigger onScrollChanged unless the scroll actually changed.) + saveScroll(); + + task.output.removeObservers(this); + task.output.observe(this, new Observer<CharSequence>() { + @Override public void onChanged(@Nullable CharSequence text) { + output(text); + } + }); + } + + private boolean isScrolledToBottom() { + int visibleHeight = (svOutput.getHeight() - svOutput.getPaddingTop() - + svOutput.getPaddingBottom()); + int maxScroll = Math.max(0, tvOutput.getHeight() - visibleHeight); + return (svOutput.getScrollY() >= maxScroll); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater mi = getMenuInflater(); + mi.inflate(resId("menu", "top_bottom"), menu); + return true; + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == resId("id", "menu_top")) { + scrollTo(Scroll.TOP); + } else if (id == resId("id", "menu_bottom")) { + scrollTo(Scroll.BOTTOM); + } else { + return false; + } + return true; + } + + public static Spannable span(CharSequence text, Object... spans) { + Spannable spanText = new SpannableStringBuilder(text); + for (Object span : spans) { + spanText.setSpan(span, 0, text.length(), 0); + } + return spanText; + } + + private void output(CharSequence text) { + removeCursor(); + if (consoleModel.pendingNewline) { + tvOutput.append("\n"); + consoleModel.pendingNewline = false; + } + if (text.charAt(text.length() - 1) == '\n') { + tvOutput.append(text.subSequence(0, text.length() - 1)); + consoleModel.pendingNewline = true; + } else { + tvOutput.append(text); + } + + Editable scrollback = (Editable) tvOutput.getText(); + if (scrollback.length() > MAX_SCROLLBACK_LEN) { + scrollback.delete(0, MAX_SCROLLBACK_LEN / 10); + } + + // Changes to the TextView height won't be reflected by getHeight until after the + // next layout pass, so isScrolledToBottom is safe here. + if (isScrolledToBottom()) { + scrollTo(Scroll.BOTTOM); + } + } + + // Don't actually scroll until the next onGlobalLayout, when we'll know what the new TextView + // height is. + private void scrollTo(Scroll request) { + // The "top" button should take priority over an auto-scroll. + if (scrollRequest != Scroll.TOP) { + scrollRequest = request; + svOutput.requestLayout(); + } + } + + // Because we've set textIsSelectable, the TextView will create an invisible cursor (i.e. a + // zero-length selection) during startup, and re-create it if necessary whenever the user taps + // on the view. When a TextView is focused and it has a cursor, it will adjust its containing + // ScrollView whenever the text changes in an attempt to keep the cursor on-screen. + // textIsSelectable implies focusable, so if there are no other focusable views in the layout, + // then it will always be focused. + // + // To avoid interference from this, we'll remove any cursor before we adjust the scroll. + // A non-zero-length selection is left untouched and may affect the scroll in the normal way, + // which is fine because it'll only exist if the user deliberately created it. + private void removeCursor() { + Spannable text = (Spannable) tvOutput.getText(); + int selStart = Selection.getSelectionStart(text); + int selEnd = Selection.getSelectionEnd(text); + + // When textIsSelectable is set, the buffer type after onRestoreInstanceState is always + // Spannable, regardless of the value of bufferType. It would then become Editable (and + // have a cursor added), during the first call to append(). Make that happen now so we can + // remove the cursor before append() is called. + if (!(text instanceof Editable)) { + tvOutput.setText(text, TextView.BufferType.EDITABLE); + text = (Editable) tvOutput.getText(); + + // setText removes any existing selection, at least on API level 26. + if (selStart >= 0) { + Selection.setSelection(text, selStart, selEnd); + } + } + + if (selStart >= 0 && selStart == selEnd) { + Selection.removeSelection(text); + } + + } + + public int resId(String type, String name) { + return Utils.resId(this, type, name); + } + + // ============================================================================================= + + public static abstract class Task extends AndroidViewModel { + + private Thread.State state = Thread.State.NEW; + + public void start() { + new Thread(() -> { + try { + Task.this.run(); + output(spanColor("[Finished]", resId("color", "console_meta"))); + } finally { + inputEnabled.postValue(false); + state = Thread.State.TERMINATED; + } + }).start(); + state = Thread.State.RUNNABLE; + } + + public Thread.State getState() { return state; } + + public MutableLiveData<Boolean> inputEnabled = new MutableLiveData<>(); + public BufferedLiveEvent<CharSequence> output = new BufferedLiveEvent<>(); + + public Task(Application app) { + super(app); + inputEnabled.setValue(false); + } + + /** Override this method to provide the task's implementation. It will be called on a + * background thread. */ + public abstract void run(); + + /** Called on the UI thread each time the user enters some input, A trailing newline is + * always included. The base class implementation does nothing. */ + public void onInput(String text) {} + + public void output(final CharSequence text) { + if (text.length() == 0) return; + output.postValue(text); + } + + public void outputError(CharSequence text) { + output(spanColor(text, resId("color", "console_error"))); + } + + public Spannable spanColor(CharSequence text, int colorId) { + int color = ContextCompat.getColor(this.getApplication(), colorId); + return span(text, new ForegroundColorSpan(color)); + } + + public int resId(String type, String name) { + return Utils.resId(getApplication(), type, name); + } + } + +} diff --git a/app/app/src/utils/java/com/chaquo/python/utils/MainActivity.java b/app/app/src/utils/java/com/chaquo/python/utils/MainActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..d47d76299bbd1ce0221f9f386df4754363725697 --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/MainActivity.java @@ -0,0 +1,40 @@ +package com.chaquo.python.utils; + +import android.content.pm.*; +import android.os.*; +import android.text.method.*; +import android.widget.*; +import androidx.appcompat.app.*; +import androidx.preference.*; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + setTitle(getTitle() + " " + version); + } catch (PackageManager.NameNotFoundException ignored) {} + + setContentView(resId("layout", "activity_menu")); + ((TextView)findViewById(resId("id", "tvCaption"))) + .setText(resId("string", "main_caption")); + getSupportFragmentManager().beginTransaction() + .replace(resId("id", "flMenu"), new MenuFragment()) + .commit(); + + ((TextView)findViewById(resId("id", "tvCaption"))).setMovementMethod(LinkMovementMethod.getInstance()); + } + + public static class MenuFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(Utils.resId(getContext(), "xml", "activity_main")); + } + } + + public int resId(String type, String name) { + return Utils.resId(this, type, name); + } +} diff --git a/app/app/src/utils/java/com/chaquo/python/utils/PythonConsoleActivity.java b/app/app/src/utils/java/com/chaquo/python/utils/PythonConsoleActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..893a914633053d6cc0a1707a67d2c4a891bbc281 --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/PythonConsoleActivity.java @@ -0,0 +1,118 @@ +package com.chaquo.python.utils; + +import android.app.*; +import android.os.*; +import android.text.*; +import android.util.*; +import android.widget.*; +import androidx.lifecycle.*; +import com.chaquo.python.*; + +/** Base class for a console-based activity that will run Python code. sys.stdout and sys.stderr + * will be directed to the output view whenever the activity is resumed. If the Python code + * caches their values, it can direct output to the activity even when it's paused. + * + * Unless inputType is InputType.TYPE_NULL, sys.stdin will also be redirected whenever + * the activity is resumed. The input box will initially be hidden, and will be displayed the + * first time sys.stdin is read. */ +public abstract class PythonConsoleActivity extends ConsoleActivity { + + protected Task task; + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + task = ViewModelProviders.of(this).get(getTaskClass()); + if (task.inputType != InputType.TYPE_NULL) { + ((TextView) findViewById(resId("id", "etInput"))).setInputType(task.inputType); + } + } + + protected abstract Class<? extends Task> getTaskClass(); + + @Override protected void onResume() { + task.resumeStreams(); + super.onResume(); // Starts the task thread. + } + + @Override protected void onPause() { + super.onPause(); + if (! isChangingConfigurations()) { + task.pauseStreams(); + } + } + + // ============================================================================================= + + public static abstract class Task extends ConsoleActivity.Task { + + protected Python py = Python.getInstance(); + private PyObject console = py.getModule("chaquopy.utils.console"); + private PyObject sys = py.getModule("sys"); + int inputType; + private PyObject stdin, stdout, stderr; + private PyObject realStdin, realStdout, realStderr; + + public Task(Application app) { + this(app, InputType.TYPE_CLASS_TEXT); + } + + public Task(Application app, int inputType) { + super(app); + this.inputType = inputType; + if (inputType != InputType.TYPE_NULL) { + realStdin = sys.get("stdin"); + stdin = console.callAttr("ConsoleInputStream", this); + } + + realStdout = sys.get("stdout"); + realStderr = sys.get("stderr"); + stdout = redirectOutput(realStdout, "output"); + stderr = redirectOutput(realStderr, "outputError"); + } + + // We're not using method references, because that would prevent using this code with + // old versions of Chaquopy. + private PyObject redirectOutput(PyObject stream, String methodName) { + return console.callAttr("ConsoleOutputStream", stream, this, methodName); + } + + public void resumeStreams() { + if (stdin != null) { + sys.put("stdin", stdin); + } + sys.put("stdout", stdout); + sys.put("stderr", stderr); + } + + public void pauseStreams() { + if (realStdin != null) { + sys.put("stdin", realStdin); + } + sys.put("stdout", realStdout); + sys.put("stderr", realStderr); + } + + @SuppressWarnings("unused") // Called from Python + public void onInputState(boolean blocked) { + if (blocked) { + inputEnabled.postValue(true); + } + } + + @Override public void onInput(String text) { + if (text != null) { + // Messages which are empty (or only consist of newlines) will not be logged. + Log.i("python.stdin", text.equals("\n") ? " " : text); + } + stdin.callAttr("on_input", text); + } + + @Override protected void onCleared() { + super.onCleared(); + if (stdin != null) { + onInput(null); // Signals EOF + } + } + } + +} diff --git a/app/app/src/utils/java/com/chaquo/python/utils/PythonTestActivity.java b/app/app/src/utils/java/com/chaquo/python/utils/PythonTestActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..d169462130d085393422fe1946506195f3a0b14d --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/PythonTestActivity.java @@ -0,0 +1,34 @@ +package com.chaquo.python.utils; + +import android.app.*; +import android.text.*; +import com.chaquo.python.*; + +public class PythonTestActivity extends PythonConsoleActivity { + + @Override protected Class<? extends Task> getTaskClass() { + return Task.class; + } + + // ============================================================================================= + + public static class Task extends PythonConsoleActivity.Task { + public Task(Application app) { + super(app, InputType.TYPE_NULL); // test_android expects stdin to return EOF. + } + + @SuppressWarnings("unused") // For pkgtest app. + public Task(Application app, int inputType) { + super(app, inputType); + } + + @Override public void run() { + PyObject unittest = py.getModule("unittest"); + PyObject runner = unittest.callAttr("TextTestRunner", new Kwarg("verbosity", 2)); + PyObject loader = unittest.get("defaultTestLoader"); + PyObject suite = loader.callAttr("loadTestsFromModule", py.getModule("chaquopy.test")); + runner.callAttr("run", suite); + } + } + +} diff --git a/app/app/src/utils/java/com/chaquo/python/utils/ReplActivity.java b/app/app/src/utils/java/com/chaquo/python/utils/ReplActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..4ebdf0b0f8bdb90da9bc4e213daee96add743323 --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/ReplActivity.java @@ -0,0 +1,40 @@ +package com.chaquo.python.utils; + +import android.app.*; +import android.text.*; + +public class ReplActivity extends PythonConsoleActivity { + + @Override protected Class<? extends Task> getTaskClass() { + return Task.class; + } + + // Maintain REPL state unless the loop has been terminated, e.g. by typing `exit()`. Requires + // the activity to be in its own task (see AndroidManifest). + @Override public void onBackPressed() { + if (task.getState() == Thread.State.RUNNABLE) { + moveTaskToBack(true); + } else { + super.onBackPressed(); + } + } + + // ============================================================================================= + + public static class Task extends PythonConsoleActivity.Task { + // VISIBLE_PASSWORD is necessary to prevent some versions of the Google keyboard from + // displaying the suggestion bar. + public Task(Application app) { + super(app, (InputType.TYPE_CLASS_TEXT + + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)); + } + + @Override public void run() { + py.getModule("chaquopy.utils.repl") + .callAttr("AndroidConsole", App.context) + .callAttr("interact"); + } + } + +} diff --git a/app/app/src/utils/java/com/chaquo/python/utils/SingleLiveEvent.java b/app/app/src/utils/java/com/chaquo/python/utils/SingleLiveEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..895be1e9a1e863246ef558ac466fcbdf946e86d5 --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/SingleLiveEvent.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017 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 com.chaquo.python.utils; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + * <p> + * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + * <p> + * Note that only one observer is going to be notified of changes. + */ +public class SingleLiveEvent<T> extends MutableLiveData<T> { + + private static final String TAG = "SingleLiveEvent"; + + private Observer<? super T> mObserver; + private Observer<T> mObserverWrapper; + + private final AtomicBoolean mPending = new AtomicBoolean(false); + + @MainThread + public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { + if (mObserver != null) { + throw new IllegalStateException + ("Cannot register multiple observers on a SingleLiveEvent"); + } + + mObserver = observer; + mObserverWrapper = new Observer<T>() { + @Override + public void onChanged(@Nullable T t) { + if (mPending.compareAndSet(true, false)) { + mObserver.onChanged(t); + } + } + }; + super.observe(owner, mObserverWrapper); + } + + @Override + public void removeObserver(@NonNull Observer<? super T> observer) { + if (observer == mObserverWrapper || // Will happen when lifecycle owner is destroyed. + observer == mObserver) { + super.removeObserver(mObserverWrapper); + mObserver = mObserverWrapper = null; + } + } + + @MainThread + public void setValue(@Nullable T t) { + mPending.set(true); + super.setValue(t); + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + public void call() { + setValue(null); + } +} \ No newline at end of file diff --git a/app/app/src/utils/java/com/chaquo/python/utils/Utils.java b/app/app/src/utils/java/com/chaquo/python/utils/Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..8e6f687d2d7a455e47b9ab0447a2067fd5a4dcce --- /dev/null +++ b/app/app/src/utils/java/com/chaquo/python/utils/Utils.java @@ -0,0 +1,14 @@ +package com.chaquo.python.utils; + +import android.content.*; +import android.content.res.*; + +public class Utils { + /** Make this package easy to copy to other apps by avoiding direct "R" references. (It + * would be better to do this by distributing it along with its resources in an AAR, but + * Chaquopy doesn't support getting Python code from an AAR yet.)*/ + public static int resId(Context context, String type, String name) { + Resources resources = context.getResources(); + return resources.getIdentifier(name, type, context.getApplicationInfo().packageName); + } +} diff --git a/app/app/src/utils/python/chaquopy/__init__.py b/app/app/src/utils/python/chaquopy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/app/src/utils/python/chaquopy/utils/__init__.py b/app/app/src/utils/python/chaquopy/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/app/src/utils/python/chaquopy/utils/console.py b/app/app/src/utils/python/chaquopy/utils/console.py new file mode 100644 index 0000000000000000000000000000000000000000..261eba0ce02e257428ea30451ee239a43f6f64c1 --- /dev/null +++ b/app/app/src/utils/python/chaquopy/utils/console.py @@ -0,0 +1,103 @@ +from io import TextIOBase +from queue import Queue + + +class ConsoleInputStream(TextIOBase): + """Receives input in on_input in one thread (non-blocking), and provides a read interface + in another thread (blocking). + """ + def __init__(self, task): + self.task = task + self.queue = Queue() + self.buffer = "" + self.eof = False + + @property + def encoding(self): + return "UTF-8" + + @property + def errors(self): + return "strict" # UTF-8 encoding should never fail. + + def readable(self): + return True + + def on_input(self, input): + if self.eof: + raise ValueError("Can't add more input after EOF") + if input is None: + self.eof = True + self.queue.put(input) + + def read(self, size=None): + if size is not None and size < 0: + size = None + buffer = self.buffer + while (self.queue is not None) and ((size is None) or (len(buffer) < size)): + if self.queue.empty(): + self.task.onInputState(True) + input = self.queue.get() + self.task.onInputState(False) + if input is None: # EOF + self.queue = None + else: + buffer += input + + result = buffer if (size is None) else buffer[:size] + self.buffer = buffer[len(result):] + return result + + def readline(self, size=None): + if size is not None and size < 0: + size = None + chars = [] + while (size is None) or (len(chars) < size): + c = self.read(1) + if not c: + break + chars.append(c) + if c == "\n": + break + + return "".join(chars) + + +class ConsoleOutputStream(TextIOBase): + """Passes each write to the underlying stream, and also to the given method, which must take + a single string argument. + """ + def __init__(self, stream, obj, method_name): + self.stream = stream + self.method = getattr(obj, method_name) + + try: + from java.android.stream import BytesOutputWrapper + except ImportError: + pass # Stay compatible with old versions of Chaquopy. + else: + self.buffer = BytesOutputWrapper(self) + + def __repr__(self): + return f"<ConsoleOutputStream {self.stream}>" + + @property + def encoding(self): + return self.stream.encoding + + @property + def errors(self): + return self.stream.errors + + def writable(self): + return self.stream.writable() + + def write(self, s): + # Pass the write to the underlying stream first, so that if it throws an exception, the + # app crashes in the same way whether it's using ConsoleOutputStream or not (#5712). + result = self.stream.write(s) + self.method(s) + return result + + def flush(self): + self.stream.flush() diff --git a/app/app/src/utils/python/chaquopy/utils/repl.py b/app/app/src/utils/python/chaquopy/utils/repl.py new file mode 100644 index 0000000000000000000000000000000000000000..bd1e948bdf698b1016c0e68306bb3b681c4cbf0d --- /dev/null +++ b/app/app/src/utils/python/chaquopy/utils/repl.py @@ -0,0 +1,18 @@ +from code import InteractiveConsole +import sys + + +class AndroidConsole(InteractiveConsole): + """`interact` must be run on a background thread, because it blocks waiting for input. + """ + def __init__(self, context): + InteractiveConsole.__init__(self, locals={"context": context.getApplicationContext()}) + + def interact(self, banner=None): + if banner is None: + banner = ("Python {} on {}\n".format(sys.version, sys.platform) + + "The current application context is available in the variable 'context'.") + try: + InteractiveConsole.interact(self, banner) + except SystemExit: + pass diff --git a/app/app/src/utils/res/drawable-hdpi/ic_launcher.png b/app/app/src/utils/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..de6b168c4e549e63de6ad83ecc7dbf1cd43a1b46 Binary files /dev/null and b/app/app/src/utils/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/app/src/utils/res/drawable-hdpi/ic_vertical_align_bottom.png b/app/app/src/utils/res/drawable-hdpi/ic_vertical_align_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..547b89a83ee66b364d74f5c52d691c748e402b0a Binary files /dev/null and b/app/app/src/utils/res/drawable-hdpi/ic_vertical_align_bottom.png differ diff --git a/app/app/src/utils/res/drawable-hdpi/ic_vertical_align_top.png b/app/app/src/utils/res/drawable-hdpi/ic_vertical_align_top.png new file mode 100644 index 0000000000000000000000000000000000000000..e046e60eff197290d2ddf0a05bab0c59ec06368b Binary files /dev/null and b/app/app/src/utils/res/drawable-hdpi/ic_vertical_align_top.png differ diff --git a/app/app/src/utils/res/drawable-mdpi/ic_launcher.png b/app/app/src/utils/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e4df75b91dfc65e13f90f61fbdd8b697ee275ec8 Binary files /dev/null and b/app/app/src/utils/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/app/src/utils/res/drawable-mdpi/ic_vertical_align_bottom.png b/app/app/src/utils/res/drawable-mdpi/ic_vertical_align_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..8129d9c84979b13b8c486f5628f87a947ffc6d5f Binary files /dev/null and b/app/app/src/utils/res/drawable-mdpi/ic_vertical_align_bottom.png differ diff --git a/app/app/src/utils/res/drawable-mdpi/ic_vertical_align_top.png b/app/app/src/utils/res/drawable-mdpi/ic_vertical_align_top.png new file mode 100644 index 0000000000000000000000000000000000000000..ee3154d695136cfaa0575028bf98540bca5e5c43 Binary files /dev/null and b/app/app/src/utils/res/drawable-mdpi/ic_vertical_align_top.png differ diff --git a/app/app/src/utils/res/drawable-xhdpi/ic_launcher.png b/app/app/src/utils/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..da669bd1ed126650354d90d10b6dacc9b7a0a3e8 Binary files /dev/null and b/app/app/src/utils/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/app/src/utils/res/drawable-xhdpi/ic_vertical_align_bottom.png b/app/app/src/utils/res/drawable-xhdpi/ic_vertical_align_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..299f7768ca824216892a55c52429a8474cdf989c Binary files /dev/null and b/app/app/src/utils/res/drawable-xhdpi/ic_vertical_align_bottom.png differ diff --git a/app/app/src/utils/res/drawable-xhdpi/ic_vertical_align_top.png b/app/app/src/utils/res/drawable-xhdpi/ic_vertical_align_top.png new file mode 100644 index 0000000000000000000000000000000000000000..b828e5385a8d71157a742e595e0cf656084332f8 Binary files /dev/null and b/app/app/src/utils/res/drawable-xhdpi/ic_vertical_align_top.png differ diff --git a/app/app/src/utils/res/drawable-xxhdpi/ic_launcher.png b/app/app/src/utils/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..441d8ce411cef94f3b48de56b3c5873d67d7e1d1 Binary files /dev/null and b/app/app/src/utils/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/app/src/utils/res/drawable-xxhdpi/ic_vertical_align_bottom.png b/app/app/src/utils/res/drawable-xxhdpi/ic_vertical_align_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..1972f48ce3f2162b1089a8063410a714ef0a5faa Binary files /dev/null and b/app/app/src/utils/res/drawable-xxhdpi/ic_vertical_align_bottom.png differ diff --git a/app/app/src/utils/res/drawable-xxhdpi/ic_vertical_align_top.png b/app/app/src/utils/res/drawable-xxhdpi/ic_vertical_align_top.png new file mode 100644 index 0000000000000000000000000000000000000000..c3208dd207821a02bff00fae87d26c73f623acd9 Binary files /dev/null and b/app/app/src/utils/res/drawable-xxhdpi/ic_vertical_align_top.png differ diff --git a/app/app/src/utils/res/layout-land/activity_menu.xml b/app/app/src/utils/res/layout-land/activity_menu.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c860527b9f2348874a825f3764233c5d7e8b788 --- /dev/null +++ b/app/app/src/utils/res/layout-land/activity_menu.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:layout_editor_absoluteY="73dp" + tools:layout_editor_absoluteX="0dp"> + + <TextView + android:id="@+id/tvCaption" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginRight="8dp" + app:layout_constraintRight_toRightOf="parent" + android:layout_marginLeft="8dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:layout_marginTop="8dp" + app:layout_constraintHorizontal_bias="0.0"/> + + <WebView + android:id="@+id/wvSource" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="0dp" + android:layout_marginLeft="0dp" + android:layout_marginRight="8dp" + android:layout_marginTop="8dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toRightOf="@+id/flMenu" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tvCaption" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHorizontal_weight="2"/> + + <FrameLayout + android:id="@+id/flMenu" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="0dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginTop="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/wvSource" + app:layout_constraintTop_toBottomOf="@+id/tvCaption" + app:layout_constraintVertical_bias="1.0" + app:layout_constraintHorizontal_weight="1"> + + </FrameLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/app/src/utils/res/layout/activity_console.xml b/app/app/src/utils/res/layout/activity_console.xml new file mode 100644 index 0000000000000000000000000000000000000000..d31a327d792c7be243211dfb0b9eb8a066aa9f8a --- /dev/null +++ b/app/app/src/utils/res/layout/activity_console.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.chaquo.python.utils.ConsoleActivity"> + + <EditText + android:id="@+id/etInput" + android:visibility="gone" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginBottom="7dp" + android:layout_marginEnd="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + android:ems="10" + android:imeOptions="actionDone|flagNoFullscreen" + android:singleLine="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + style="@style/Code"> + <requestFocus/> + </EditText> + + <ScrollView + android:id="@+id/svOutput" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="0dp" + android:layout_marginLeft="0dp" + android:layout_marginRight="0dp" + android:layout_marginTop="0dp" + app:layout_constraintBottom_toTopOf="@id/etInput" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.0"> + + <TextView + android:id="@+id/tvOutput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/ConsoleOutput"/> + </ScrollView> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/app/src/utils/res/layout/activity_menu.xml b/app/app/src/utils/res/layout/activity_menu.xml new file mode 100644 index 0000000000000000000000000000000000000000..10d062070a7922d4b6e647443a9c35281b11598a --- /dev/null +++ b/app/app/src/utils/res/layout/activity_menu.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/tvCaption" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintHorizontal_bias="0.0"/> + + <WebView + android:id="@+id/wvSource" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="0dp" + android:layout_marginEnd="8dp" + android:layout_marginLeft="0dp" + android:layout_marginRight="0dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/flMenu" + app:layout_constraintVertical_bias="0.0"/> + + <FrameLayout + android:id="@+id/flMenu" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginEnd="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + app:layout_constraintBottom_toTopOf="@id/wvSource" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tvCaption" + android:layout_marginTop="8dp" + app:layout_constraintVertical_bias="0.0" + android:layout_marginBottom="0dp"> + + </FrameLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/app/src/utils/res/menu/top_bottom.xml b/app/app/src/utils/res/menu/top_bottom.xml new file mode 100644 index 0000000000000000000000000000000000000000..babc5538036a4428b2b7992d33eca19638421bf5 --- /dev/null +++ b/app/app/src/utils/res/menu/top_bottom.xml @@ -0,0 +1,14 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item android:id="@+id/menu_top" + android:title="@string/top" + android:icon="@drawable/ic_vertical_align_top" + app:showAsAction="ifRoom"/> + + <item android:id="@+id/menu_bottom" + android:title="@string/bottom" + android:icon="@drawable/ic_vertical_align_bottom" + app:showAsAction="ifRoom"/> + +</menu> \ No newline at end of file diff --git a/app/app/src/utils/res/values-sw360dp-v13/support.xml b/app/app/src/utils/res/values-sw360dp-v13/support.xml new file mode 100644 index 0000000000000000000000000000000000000000..ede5ac1cf39e15098e4b4a33d84ebf9a4c783093 --- /dev/null +++ b/app/app/src/utils/res/values-sw360dp-v13/support.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools"> + + <!-- Remove padding on left (https://stackoverflow.com/a/52960668). --> + <bool name="config_materialPreferenceIconSpaceReserved" + tools:ignore="MissingDefaultResource,PrivateResource">false</bool> + +</resources> \ No newline at end of file diff --git a/app/app/src/utils/res/values/console.xml b/app/app/src/utils/res/values/console.xml new file mode 100644 index 0000000000000000000000000000000000000000..e8943b8b40a82818bbed8f03b6bdbd07915be970 --- /dev/null +++ b/app/app/src/utils/res/values/console.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="repl">Python console</string> + <string name="repl_summary">Interactive read-eval-print loop</string> + + <string name="top">Top</string> + <string name="bottom">Bottom</string> + + <color name="console_error">#b53f3f</color> + <color name="console_meta">#304ffe</color> + + <style name="Code"> + <item name="android:textSize">14sp</item> + <item name="android:typeface">monospace</item> + + <!-- In API level 21 only, the default sans-serif fontFamily takes priority over the + `typeface` attribute (https://stackoverflow.com/q/29076599). --> + <item name="android:fontFamily">monospace</item> + </style> + + <style name="ConsoleOutput" parent="Code"> + <item name="android:layout_marginLeft">8dp</item> + <item name="android:layout_marginRight">8dp</item> + <item name="android:textIsSelectable">true</item> + <item name="android:freezesText">true</item> + </style> + +</resources> diff --git a/app/app/src/utils/res/values/utils.xml b/app/app/src/utils/res/values/utils.xml new file mode 100644 index 0000000000000000000000000000000000000000..94e92a9569228f884a00ff0f851b469bc69b04d0 --- /dev/null +++ b/app/app/src/utils/res/values/utils.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Base application theme. --> + <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> + <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item> + + <item name="colorPrimary">@color/colorPrimary</item> + <item name="colorPrimaryDark">@color/colorPrimaryDark</item> + <item name="colorAccent">@color/colorAccent</item> + </style> + + <color name="colorPrimary">#3F51B5</color> + <color name="colorPrimaryDark">#303F9F</color> + <color name="colorAccent">#FF4081</color> + + <string name="main_caption">For more information see the + <a href="https://chaquo.com/chaquopy/">Chaquopy website</a>, or view this app\'s source + code <a href="https://github.com/chaquo/chaquopy/tree/master/demo">on GitHub</a>. + </string> + +</resources> \ No newline at end of file