diff --git a/.gitignore b/.gitignore index 63cb77a..6d6c6ca 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /build /captures .externalNativeBuild +.kotlin diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 9219fb8..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdk 34 - defaultConfig { - applicationId "org.cuberite.android" - resourceConfigurations += ['en', 'de', 'nl', 'pt', 'zh_CN'] - minSdk 16 - //noinspection ExpiredTargetSdkVersion - targetSdk 28 - versionCode 15 - versionName "1.6.3" - vectorDrawables.useSupportLibrary = true - } - buildFeatures { - buildConfig true - } - buildTypes { - release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile( - 'proguard-android-optimize.txt'), - 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - namespace 'org.cuberite.android' -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core:1.12.0' - implementation 'androidx.fragment:fragment:1.6.2' - implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' - implementation 'androidx.preference:preference:1.2.1' - implementation 'androidx.vectordrawable:vectordrawable:1.1.0' - implementation 'com.google.android.material:material:1.10.0' - implementation 'org.ini4j:ini4j:0.5.4' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..6793cfb --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "org.cuberite.android" + compileSdk = 34 + defaultConfig { + applicationId = "org.cuberite.android" + resourceConfigurations += listOf("en", "de", "nl", "pt", "zh_CN") + minSdk = 16 + //noinspection ExpiredTargetSdkVersion + targetSdk = 28 + versionCode = 15 + versionName = "1.6.3" + vectorDrawables.useSupportLibrary = true + } + buildFeatures { + buildConfig = true + } + buildTypes { + getByName("release") { + signingConfig = signingConfigs.getByName("debug") + } + named("release") { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.vectordrawable:vectordrawable:1.1.0") + implementation("com.google.android.material:material:1.10.0") + implementation("org.ini4j:ini4j:0.5.4") +} diff --git a/app/src/main/java/org/cuberite/android/MainActivity.java b/app/src/main/java/org/cuberite/android/MainActivity.java deleted file mode 100644 index f10989c..0000000 --- a/app/src/main/java/org/cuberite/android/MainActivity.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.cuberite.android; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.util.Log; -import android.view.MenuItem; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; - -import com.google.android.material.bottomnavigation.BottomNavigationView; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.elevation.SurfaceColors; -import com.google.android.material.snackbar.Snackbar; - -import org.cuberite.android.fragments.ConsoleFragment; -import org.cuberite.android.fragments.ControlFragment; -import org.cuberite.android.fragments.SettingsFragment; - -public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnItemSelectedListener { - // Logging tag - private final String LOG = "Cuberite/MainActivity"; - - private SharedPreferences preferences; - private AlertDialog permissionPopup; - private static BottomNavigationView navigation; - - private String PRIVATE_DIR; - private String PUBLIC_DIR; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.container); - - if (savedInstanceState == null) { - loadFragment(new ControlFragment()); - } - - // Set colors - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - getWindow().setStatusBarColor(SurfaceColors.SURFACE_0.getColor(this)); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - getWindow().setNavigationBarColor(SurfaceColors.SURFACE_2.getColor(this)); - } - - // Set navigation bar listener - navigation = findViewById(R.id.bottom_navigation); - navigation.setOnItemSelectedListener(this); - - // Initialize settings - preferences = getSharedPreferences(this.getPackageName(), MODE_PRIVATE); - - PRIVATE_DIR = this.getFilesDir().getAbsolutePath(); - PUBLIC_DIR = Environment.getExternalStorageDirectory().getAbsolutePath(); - } - - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem item) { - Fragment fragment = null; - - if (item.getItemId() == R.id.item_control) { - fragment = new ControlFragment(); - } - else if (item.getItemId() == R.id.item_console) { - fragment = new ConsoleFragment(); - } - else if (item.getItemId() == R.id.item_settings) { - fragment = new SettingsFragment(); - } - - return loadFragment(fragment); - } - - private boolean loadFragment(Fragment fragment) { - if (fragment != null) { - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.fragment_container, fragment) - .commit(); - return true; - } - return false; - } - - public static void showSnackBar(Context context, String message) { - Snackbar.make(((Activity) context).findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) - .setAnchorView(MainActivity.navigation) - .show(); - } - - private final ActivityResultLauncher requestPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - Log.i(LOG, "Got permissions, using public directory"); - preferences.edit().putString("cuberiteLocation", PUBLIC_DIR + "/cuberite-server").apply(); - } else { - Log.i(LOG, "Permissions denied, boo, using private directory"); - preferences.edit().putString("cuberiteLocation", PRIVATE_DIR + "/cuberite-server").apply(); - } - }); - - private void showPermissionPopup() { - permissionPopup = new MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.status_permissions_needed)) - .setMessage(R.string.message_externalstorage_permission) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, id) -> { - Log.d(LOG, "Requesting permissions for external storage"); - permissionPopup = null; - requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE); - }) - .create(); - - permissionPopup.show(); - } - - private void checkPermissions() { - final String location = preferences.getString("cuberiteLocation", ""); - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - // User is running Android 6 or above, show permission popup on first run - // or if user granted permission and later denied it - - if (location.isEmpty() || location.startsWith(PUBLIC_DIR)) { - showPermissionPopup(); - } - } else if (location.isEmpty() || location.startsWith(PRIVATE_DIR)) { - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("cuberiteLocation", PUBLIC_DIR + "/cuberite-server"); - editor.apply(); - } - } - - @Override - public void onPause() { - super.onPause(); - if (permissionPopup != null) { - permissionPopup.dismiss(); - permissionPopup = null; - } - } - - @Override - public void onResume() { - super.onResume(); - checkPermissions(); - } -} diff --git a/app/src/main/java/org/cuberite/android/MainApplication.java b/app/src/main/java/org/cuberite/android/MainApplication.java deleted file mode 100644 index 0ef1f32..0000000 --- a/app/src/main/java/org/cuberite/android/MainApplication.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.cuberite.android; - -import android.app.Application; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; - -import androidx.appcompat.app.AppCompatDelegate; - -import com.google.android.material.color.DynamicColors; - -public class MainApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // Application theme - final SharedPreferences preferences = getSharedPreferences(this.getPackageName(), MODE_PRIVATE); - AppCompatDelegate.setDefaultNightMode(preferences.getInt("defaultTheme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)); - DynamicColors.applyToActivitiesIfAvailable(this); - - // Notification channel - createNotificationChannel(); - } - - private void createNotificationChannel() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return; - } - - final String channelId = "cuberiteservice"; - final CharSequence name = getString(R.string.app_name); - - final NotificationChannel channel = new NotificationChannel( - channelId, - name, - NotificationManager.IMPORTANCE_HIGH - ); - channel.setSound(null, null); - channel.setVibrationPattern(new long[]{0}); - channel.enableVibration(true); - - final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.createNotificationChannel(channel); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/cuberite/android/fragments/ConsoleFragment.java b/app/src/main/java/org/cuberite/android/fragments/ConsoleFragment.java deleted file mode 100644 index 14696ac..0000000 --- a/app/src/main/java/org/cuberite/android/fragments/ConsoleFragment.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.cuberite.android.fragments; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Color; -import android.os.Bundle; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.style.ForegroundColorSpan; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.appcompat.widget.TooltipCompat; -import androidx.fragment.app.Fragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.android.material.color.MaterialColors; -import com.google.android.material.textfield.TextInputLayout; - -import org.cuberite.android.R; -import org.cuberite.android.helpers.CuberiteHelper; - -public class ConsoleFragment extends Fragment { - private TextView logView; - private EditText inputLine; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_console, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - logView = view.findViewById(R.id.logView); - - inputLine = view.findViewById(R.id.inputLine); - inputLine.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - String command = inputLine.getText().toString(); - sendExecuteCommand(command); - inputLine.setText(""); - // return true makes sure the keyboard doesn't close - return true; - } - return false; - }); - - final TextInputLayout textInputLayout = view.findViewById(R.id.inputWrapper); - textInputLayout.setEndIconOnClickListener(v -> { - String command = inputLine.getText().toString(); - sendExecuteCommand(command); - inputLine.setText(""); - }); - } - - private void sendExecuteCommand(String command) { - if (!command.isEmpty() - && CuberiteHelper.isCuberiteRunning(requireActivity())) { - // Logging tag - String LOG = "Cuberite/Console"; - - Log.d(LOG, "Executing " + command); - Intent intent = new Intent("executeCommand"); - intent.putExtra("message", command); - LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent); - } - } - - // Broadcast receivers - private final BroadcastReceiver updateLog = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final ScrollView scrollView = (ScrollView) logView.getParent(); - boolean shouldScroll = (logView.getBottom() - (scrollView.getHeight() + scrollView.getScrollY())) <= 0; - String output = CuberiteHelper.getConsoleOutput(); - SpannableStringBuilder formattedOutput = new SpannableStringBuilder(); - - for (String line : output.split("\\n")) { - if (line.isEmpty()) { - continue; - } - - if (formattedOutput.length() > 0) { - // Line break - formattedOutput.append("\n"); - } - - int color = -1; - - if (line.toLowerCase().startsWith("log: ")) { - line = line.replaceFirst("(?i)log: ", ""); - } - else if (line.toLowerCase().startsWith("info: ")) { - line = line.replaceFirst("(?i)info: ", ""); - color = R.attr.colorTertiary; - } - else if (line.toLowerCase().startsWith("warning: ")) { - line = line.replaceFirst("(?i)warning: ", ""); - color = R.attr.colorError; - } - else if (line.toLowerCase().startsWith("error: ")) { - line = line.replaceFirst("(?i)error: ", ""); - color = R.attr.colorOnErrorContainer; - } - - SpannableStringBuilder logLine = new SpannableStringBuilder(line); - - if (color >= 0) { - int start = 0; - int end = logLine.length(); - color = MaterialColors.getColor(requireContext(), color, Color.BLACK); - - logLine.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - formattedOutput.append(logLine); - } - logView.setText(formattedOutput); - - if (shouldScroll) { - scrollView.post(() -> { - scrollView.fullScroll(ScrollView.FOCUS_DOWN); - inputLine.requestFocus(); - }); - } - } - }; - - @Override - public void onPause() { - super.onPause(); - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(updateLog); - } - - @Override - public void onResume() { - super.onResume(); - LocalBroadcastManager.getInstance(requireContext()).registerReceiver(updateLog, new IntentFilter("updateLog")); - LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(new Intent("updateLog")); - } -} diff --git a/app/src/main/java/org/cuberite/android/fragments/ControlFragment.java b/app/src/main/java/org/cuberite/android/fragments/ControlFragment.java deleted file mode 100644 index 0e2b943..0000000 --- a/app/src/main/java/org/cuberite/android/fragments/ControlFragment.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.cuberite.android.fragments; - -import android.animation.ArgbEvaluator; -import android.animation.ValueAnimator; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import com.google.android.material.color.MaterialColors; - -import org.cuberite.android.MainActivity; -import org.cuberite.android.helpers.CuberiteHelper; -import org.cuberite.android.helpers.InstallHelper; -import org.cuberite.android.helpers.StateHelper; -import org.cuberite.android.helpers.StateHelper.State; -import org.cuberite.android.R; - -public class ControlFragment extends Fragment { - // Logging tag - private final String LOG = "Cuberite/Control"; - - private Button mainButton; - private int mainButtonColor; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_control, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - mainButton = view.findViewById(R.id.mainButton); - mainButtonColor = MaterialColors.getColor(mainButton, R.attr.colorSurface); - } - - private void animateColorChange(final View view, int colorFrom, int colorTo) { - Log.d(LOG, "Changing color from " + Integer.toHexString(colorFrom) + " to " + Integer.toHexString(colorTo)); - - ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - colorAnimation.setDuration(300); - colorAnimation.addUpdateListener(animator -> view.setBackgroundColor((int) animator.getAnimatedValue())); - colorAnimation.start(); - mainButtonColor = colorTo; - } - - private void setInstallButton(final State state) { - int colorTo = MaterialColors.getColor(mainButton, R.attr.colorPrimary); - animateColorChange(mainButton, mainButtonColor, colorTo); - mainButton.setText(getText(R.string.do_install_cuberite)); - mainButton.setOnClickListener(view -> { - LocalBroadcastManager.getInstance(requireContext()).registerReceiver( - installServiceCallback, - new IntentFilter("InstallService.callback") - ); - - InstallHelper.installCuberiteDownload(requireActivity(), state); - }); - } - - private void setStartButton() { - int colorTo = MaterialColors.getColor(mainButton, R.attr.colorPrimary); - animateColorChange(mainButton, mainButtonColor, colorTo); - mainButton.setText(getText(R.string.do_start_cuberite)); - mainButton.setOnClickListener(view -> { - LocalBroadcastManager.getInstance(requireContext()).registerReceiver( - showStartupError, - new IntentFilter("showStartupError") - ); - - CuberiteHelper.startCuberite(requireContext()); - setStopButton(); - }); - } - - private void setStopButton() { - int colorTo = MaterialColors.getColor(mainButton, R.attr.colorTertiary); - animateColorChange(mainButton, mainButtonColor, colorTo); - mainButton.setText(getText(R.string.do_stop_cuberite)); - mainButton.setOnClickListener(view -> { - CuberiteHelper.stopCuberite(requireContext()); - setKillButton(); - }); - } - - private void setKillButton() { - int colorTo = MaterialColors.getColor(mainButton, R.attr.colorError); - animateColorChange(mainButton, mainButtonColor, colorTo); - mainButton.setText(getText(R.string.do_kill_cuberite)); - mainButton.setOnClickListener(view -> CuberiteHelper.killCuberite(requireContext())); - } - - private void updateControlButton() { - final State state = StateHelper.getState(requireContext()); - - if (state == State.RUNNING) { - setStopButton(); - } else if (state == State.READY) { - setStartButton(); - } else { - setInstallButton(state); - } - } - - // Broadcast receivers - private final BroadcastReceiver cuberiteServiceCallback = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - updateControlButton(); - } - }; - - private final BroadcastReceiver installServiceCallback = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - LocalBroadcastManager.getInstance(context).unregisterReceiver(this); - String result = intent.getStringExtra("result"); - MainActivity.showSnackBar(requireContext(), result); - updateControlButton(); - } - }; - - private final BroadcastReceiver showStartupError = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - LocalBroadcastManager.getInstance(context).unregisterReceiver(this); - Log.d(LOG, "Cuberite exited on process"); - MainActivity.showSnackBar( - requireContext(), - String.format( - getString(R.string.status_failed_start), - CuberiteHelper.getPreferredABI() - ) - ); - } - }; - - // Register/unregister receivers and update button state - @Override - public void onPause() { - super.onPause(); - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(cuberiteServiceCallback); - } - - @Override - public void onResume() { - super.onResume(); - LocalBroadcastManager.getInstance(requireContext()).registerReceiver(cuberiteServiceCallback, new IntentFilter("CuberiteService.callback")); - updateControlButton(); - } -} diff --git a/app/src/main/java/org/cuberite/android/fragments/SettingsFragment.java b/app/src/main/java/org/cuberite/android/fragments/SettingsFragment.java deleted file mode 100644 index 9a96bcc..0000000 --- a/app/src/main/java/org/cuberite/android/fragments/SettingsFragment.java +++ /dev/null @@ -1,443 +0,0 @@ -package org.cuberite.android.fragments; - -import android.Manifest; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.util.Log; -import android.view.View; -import android.widget.EditText; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreferenceCompat; - -import org.cuberite.android.BuildConfig; -import org.cuberite.android.MainActivity; -import org.cuberite.android.R; -import org.cuberite.android.helpers.CuberiteHelper; -import org.cuberite.android.helpers.InstallHelper; -import org.cuberite.android.helpers.StateHelper.State; -import org.ini4j.Config; -import org.ini4j.Ini; - -import java.io.File; -import java.io.IOException; - -import static android.content.Context.MODE_PRIVATE; -import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; -import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; -import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -public class SettingsFragment extends PreferenceFragmentCompat { - // Logging tag - private final String LOG = "Cuberite/Settings"; - - @Override - public void onCreatePreferences(Bundle bundle, String s) { - addPreferencesFromResource(R.xml.preferences); - - final SharedPreferences preferences = requireContext().getSharedPreferences(requireContext().getPackageName(), MODE_PRIVATE); - - // Ini4j config - Config config = Config.getGlobal(); - config.setEscape(false); - config.setStrictOperator(true); - - // Initialize - initializeThemeSettings(preferences); - initializeStartupSettings(preferences); - initializeSDCardSettings(preferences); - initializeWebadminSettings(preferences); - initializeInstallSettings(); - initializeInfoSettings(preferences); - } - - - // Theme-related methods - - private void initializeThemeSettings(final SharedPreferences preferences) { - int getCurrentTheme = preferences.getInt("defaultTheme", MODE_NIGHT_FOLLOW_SYSTEM); - - ListPreference theme = findPreference("theme"); - theme.setDialogTitle(getString(R.string.settings_theme_choose)); - theme.setEntries(new CharSequence[]{ - getString(R.string.settings_theme_light), - getString(R.string.settings_theme_dark), - getString(R.string.settings_theme_auto) - }); - theme.setEntryValues(new CharSequence[]{"light", "dark", "auto"}); - - switch (getCurrentTheme) { - case MODE_NIGHT_NO -> theme.setValue("light"); - case MODE_NIGHT_YES -> theme.setValue("dark"); - default -> theme.setValue("auto"); - } - - theme.setOnPreferenceChangeListener((preference, newValue) -> { - int newTheme = switch (newValue.toString()) { - case "light" -> MODE_NIGHT_NO; - case "dark" -> MODE_NIGHT_YES; - default -> MODE_NIGHT_FOLLOW_SYSTEM; - }; - - AppCompatDelegate.setDefaultNightMode(newTheme); - SharedPreferences.Editor editor = preferences.edit(); - editor.putInt("defaultTheme", newTheme); - editor.apply(); - return true; - }); - } - - - // Startup-related methods - - private void initializeStartupSettings(final SharedPreferences preferences) { - final SwitchPreferenceCompat startupToggle = findPreference("startupToggle"); - startupToggle.setOnPreferenceChangeListener((preference, newValue) -> { - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean("startOnBoot", (boolean) newValue); - editor.apply(); - return true; - }); - } - - - // SD Card-related methods - - private void initializeSDCardSettings(final SharedPreferences preferences) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return; - } - - final String PUBLIC_DIR = Environment.getExternalStorageDirectory().getAbsolutePath(); - final String PRIVATE_DIR = requireContext().getFilesDir().getAbsolutePath(); - final String location = preferences.getString("cuberiteLocation", ""); - final boolean isSDAvailable = requireContext().getExternalFilesDirs(null).length > 1 - && requireContext().getExternalFilesDirs(null)[1] != null; - final boolean isSDEnabled = !(location.startsWith(PUBLIC_DIR) || location.startsWith(PRIVATE_DIR)); - final SwitchPreferenceCompat toggleSD = findPreference("saveToSDToggle"); - - if (!(isSDAvailable || isSDEnabled)) { - return; - } - - Log.d(LOG, "SD Card found or location set, showing preference"); - toggleSD.setVisible(true); - toggleSD.setChecked(isSDEnabled); - - toggleSD.setOnPreferenceChangeListener((preference, newValue) -> { - if (CuberiteHelper.isCuberiteRunning(requireContext())) { - MainActivity.showSnackBar( - requireContext(), - getString(R.string.settings_sd_card_running) - ); - return false; - } - - final boolean isSDAvailableInner = requireContext().getExternalFilesDirs(null).length > 1 - && requireContext().getExternalFilesDirs(null)[1] != null; - String newLocation = PUBLIC_DIR; - - if ((boolean) newValue && isSDAvailableInner) { - // SD dir - newLocation = requireContext().getExternalFilesDirs(null)[1].getAbsolutePath(); - } else { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - // Private dir - newLocation = requireContext().getFilesDir().getAbsolutePath(); - } - toggleSD.setVisible(isSDAvailableInner); - } - - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("cuberiteLocation", newLocation + "/cuberite-server"); - editor.apply(); - return true; - }); - } - - - // Webadmin-related methods - - private void initializeWebadminSettings(final SharedPreferences preferences) { - final File webadminFile = getWebadminFile(preferences); - final String url = getWebadminUrl(webadminFile); - - if (url != null) { - Preference webadminDescription = findPreference("webadminDescription"); - webadminDescription.setSummary(webadminDescription.getSummary() + "\n\n" + "URL: " + url); - } - - Preference webadminOpen = findPreference("webadminOpen"); - webadminOpen.setOnPreferenceClickListener(preference -> { - if (!CuberiteHelper.isCuberiteRunning(requireContext())) { - MainActivity.showSnackBar( - requireContext(), - getString(R.string.settings_webadmin_not_running) - ); - return true; - } - - final File webadminFileInner = getWebadminFile(preferences); - final String urlInner = getWebadminUrl(webadminFileInner); - - if (urlInner != null) { - Log.d(LOG, "Opening Webadmin on " + urlInner); - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlInner)); - startActivity(browserIntent); - } - return true; - }); - - Preference webadminLogin = findPreference("webadminLogin"); - webadminLogin.setOnPreferenceClickListener(preference -> { - try { - final File webadminFileInner = getWebadminFile(preferences); - final Ini ini = createWebadminIni(webadminFileInner); - ini.put("WebAdmin", "Enabled", 1); - - showWebadminCredentialPopup(webadminFileInner, ini); - } catch(IOException e) { - Log.e(LOG, "Something went wrong while opening the ini file", e); - MainActivity.showSnackBar( - requireContext(), - getString(R.string.settings_webadmin_error) - ); - } - return true; - }); - } - - private Ini createWebadminIni(final File webadminFile) throws IOException { - Ini ini; - if (!webadminFile.exists()) { - ini = new Ini(); - ini.put("WebAdmin", "Ports", 8080); - ini.put("WebAdmin", "Enabled", 1); - ini.store(webadminFile); - } else { - ini = new Ini(webadminFile); - } - - return ini; - } - - private File getWebadminFile(final SharedPreferences preferences) { - final File cuberiteDir = new File(preferences.getString("cuberiteLocation", "")); - return new File(cuberiteDir, "webadmin.ini"); - } - - private String getWebadminUrl(final File webadminFile) { - String url = null; - - try { - final Ini ini = createWebadminIni(webadminFile); - final String ip = CuberiteHelper.getIpAddress(requireContext()); - int port; - - try { - port = Integer.parseInt(ini.get("WebAdmin", "Ports")); - } catch (NumberFormatException e) { - ini.put("WebAdmin", "Ports", 8080); - ini.store(webadminFile); - port = Integer.parseInt(ini.get("WebAdmin", "Ports")); - } - - url = "http://" + ip + ":" + port; - } catch (IOException e) { - Log.e(LOG, "Something went wrong while opening the ini file", e); - } - return url; - } - - private void showWebadminCredentialPopup(final File webadminFile, final Ini ini) { - String username = ""; - String password = ""; - - for (String sectionName : ini.keySet()) { - if (sectionName.startsWith("User:")) { - username = sectionName.substring(5); - password = ini.get(sectionName, "Password"); - } - } - final String oldUsername = username; - - final View layout = View.inflate(requireContext(), R.layout.dialog_webadmin_credentials, null); - ((EditText) layout.findViewById(R.id.webadminUsername)).setText(username); - ((EditText) layout.findViewById(R.id.webadminPassword)).setText(password); - - final AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext()) - .setView(layout) - .setTitle(R.string.settings_webadmin_login) - .setPositiveButton(R.string.ok, (dialog12, id) -> { - String newUsername = ((EditText) layout.findViewById(R.id.webadminUsername)).getText().toString(); - String newPassword = ((EditText) layout.findViewById(R.id.webadminPassword)).getText().toString(); - - ini.remove("User:" + oldUsername); - ini.put("User:" + newUsername, "Password", newPassword); - - try { - ini.store(webadminFile); - MainActivity.showSnackBar( - requireContext(), - getString(R.string.settings_webadmin_success) - ); - } catch(IOException e) { - Log.e(LOG, "Something went wrong while saving the ini file", e); - MainActivity.showSnackBar( - requireContext(), - getString(R.string.settings_webadmin_error) - ); - } - }) - .setNegativeButton(R.string.cancel, (dialog1, id) -> dialog1.cancel()) - .create(); - dialog.show(); - } - - - // Install-related methods - - private void initializeInstallSettings() { - Preference updateBinary = findPreference("installUpdateBinary"); - updateBinary.setOnPreferenceClickListener(preference -> { - InstallHelper.installCuberiteDownload(requireActivity(), State.NEED_DOWNLOAD_BINARY); - return true; - }); - - Preference updateServer = findPreference("installUpdateServer"); - updateServer.setOnPreferenceClickListener(preference -> { - InstallHelper.installCuberiteDownload(requireActivity(), State.NEED_DOWNLOAD_SERVER); - return true; - }); - - String abi = String.format(getString(R.string.settings_install_manually_abi), CuberiteHelper.getPreferredABI()); - Preference setABIText = findPreference("abiText"); - setABIText.setSummary(setABIText.getSummary() + "\n\n" + abi); - - Preference installBinary = findPreference("installBinary"); - installBinary.setOnPreferenceClickListener(preference -> { - pickFile(pickFileBinaryLauncher); - return true; - }); - - Preference installServer = findPreference("installServer"); - installServer.setOnPreferenceClickListener(preference -> { - pickFile(pickFileServerLauncher); - return true; - }); - } - - private final BroadcastReceiver installServiceCallback = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String result = intent.getStringExtra("result"); - MainActivity.showSnackBar(requireContext(), result); - } - }; - - private final ActivityResultLauncher pickFileBinaryLauncher = - registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> InstallHelper.installCuberiteLocal(requireActivity(), State.PICK_FILE_BINARY, uri)); - - private final ActivityResultLauncher pickFileServerLauncher = - registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> InstallHelper.installCuberiteLocal(requireActivity(), State.PICK_FILE_SERVER, uri)); - - private void pickFile(ActivityResultLauncher launcher) { - try { - launcher.launch("*/*"); - } catch (ActivityNotFoundException e) { - MainActivity.showSnackBar( - requireContext(), - requireContext().getString(R.string.status_missing_filemanager) - ); - } - } - - - // Info-related methods - - private void initializeInfoSettings(final SharedPreferences preferences) { - Preference infoDebugInfo = findPreference("infoDebugInfo"); - infoDebugInfo.setOnPreferenceClickListener(preference -> { - final String title = getString(R.string.settings_info_debug); - final String message = "Running on Android " + Build.VERSION.RELEASE + " (API Level " + Build.VERSION.SDK_INT + ")\n" + - "Using ABI " + CuberiteHelper.getPreferredABI() + "\n" + - "IP: " + CuberiteHelper.getIpAddress(requireContext()) + "\n" + - "Private directory: " + requireContext().getFilesDir() + "\n" + - "Public directory: " + Environment.getExternalStorageDirectory() + "\n" + - "Storage location: " + preferences.getString("cuberiteLocation", "") + "\n" + - "Download URL: " + InstallHelper.getDownloadHost(); - showInfoPopup(title, message); - return true; - }); - - Preference thirdPartyLicenses = findPreference("thirdPartyLicenses"); - thirdPartyLicenses.setOnPreferenceClickListener(preference -> { - final String title = getString(R.string.settings_info_libraries); - final String message = getString(R.string.ini4j_license) + "\n\n" + - getString(R.string.ini4j_license_description); - showInfoPopup(title, message); - return true; - }); - - Preference version = findPreference("version"); - version.setSummary(String.format(getString(R.string.settings_info_version), BuildConfig.VERSION_NAME)); - version.setOnPreferenceClickListener(preference -> { - final Intent browserIntent = new Intent( - Intent.ACTION_VIEW, - Uri.parse("https://download.cuberite.org/android") - ); - startActivity(browserIntent); - return true; - }); - } - - private void showInfoPopup(String title, String message) { - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.ok, (dialog1, id) -> { - if (dialog1 != null) { - dialog1.dismiss(); - } - }) - .show(); - } - - - // Listeners - - @Override - public void onPause() { - super.onPause(); - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(installServiceCallback); - } - - @Override - public void onResume() { - super.onResume(); - - LocalBroadcastManager.getInstance(requireContext()).registerReceiver( - installServiceCallback, - new IntentFilter("InstallService.callback") - ); - } -} diff --git a/app/src/main/java/org/cuberite/android/helpers/CuberiteHelper.java b/app/src/main/java/org/cuberite/android/helpers/CuberiteHelper.java deleted file mode 100644 index 02b9f8e..0000000 --- a/app/src/main/java/org/cuberite/android/helpers/CuberiteHelper.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.cuberite.android.helpers; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.Intent; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Build; -import android.text.format.Formatter; -import android.util.Log; - -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.cuberite.android.services.CuberiteService; - -import static android.content.Context.WIFI_SERVICE; - -public class CuberiteHelper { - // Logging tag - private static final String LOG = "Cuberite/CuberiteHelper"; - - private static StringBuilder consoleOutput = new StringBuilder(); - - private static final String EXECUTABLE_NAME = "Cuberite"; - - public static void addConsoleOutput(Context context, String string) { - consoleOutput.append(string).append("\n"); - - Intent intent = new Intent("updateLog"); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - - public static String getConsoleOutput() { - return consoleOutput.toString(); - } - - public static void resetConsoleOutput() { - consoleOutput = new StringBuilder(); - } - - public static String getExecutableName() { - return EXECUTABLE_NAME; - } - - public static String getIpAddress(Context context) { - WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE); - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - int ip = wifiInfo.getIpAddress(); - - if (ip == 0) { - return "127.0.0.1"; - } else { - return Formatter.formatIpAddress(ip); - } - } - - public static String getPreferredABI() { - String abi; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - abi = Build.SUPPORTED_ABIS[0]; - } else { - abi = Build.CPU_ABI; - } - - Log.d(LOG, "Getting preferred ABI: " + abi); - - return abi; - } - - public static boolean isCuberiteRunning(Context context) { - ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - - for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (CuberiteService.class.getName().equals(service.service.getClassName())) { - return true; - } - } - return false; - } - - public static void startCuberite(Context context) { - Log.d(LOG, "Starting Cuberite"); - - Intent serviceIntent = new Intent(context, CuberiteService.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(serviceIntent); - } else { - context.startService(serviceIntent); - } - } - - public static void stopCuberite(Context context) { - Log.d(LOG, "Stopping Cuberite"); - - LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent("stop")); - } - - public static void killCuberite(Context context) { - LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent("kill")); - } -} diff --git a/app/src/main/java/org/cuberite/android/helpers/InstallHelper.java b/app/src/main/java/org/cuberite/android/helpers/InstallHelper.java deleted file mode 100644 index 56bb882..0000000 --- a/app/src/main/java/org/cuberite/android/helpers/InstallHelper.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.cuberite.android.helpers; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Handler; - -import org.cuberite.android.helpers.StateHelper.State; -import org.cuberite.android.receivers.ProgressReceiver; -import org.cuberite.android.services.InstallService; - -public class InstallHelper { - private static final String DOWNLOAD_HOST = "https://download.cuberite.org/androidbinaries/"; - - public static String getDownloadHost() { - return DOWNLOAD_HOST; - } - - public static void installCuberiteDownload(final Activity activity, State state) { - SharedPreferences preferences = activity.getSharedPreferences(activity.getPackageName(), Context.MODE_PRIVATE); - - Intent intent = new Intent(activity, InstallService.class) - .setAction("download") - .putExtra("downloadHost", DOWNLOAD_HOST) - .putExtra("state", state) - .putExtra("targetFolder", preferences.getString("cuberiteLocation", "")) - .putExtra("receiver", new ProgressReceiver(activity, new Handler())); - - activity.startService(intent); - } - - public static void installCuberiteLocal(Activity activity, State state, Uri selectedFileUri) { - SharedPreferences preferences = activity.getSharedPreferences(activity.getPackageName(), Context.MODE_PRIVATE); - - if (selectedFileUri != null) { - Intent intent = new Intent(activity, InstallService.class) - .setAction("unzip") - .putExtra("uri", selectedFileUri) - .putExtra("state", state) - .putExtra("targetFolder", preferences.getString("cuberiteLocation", "")) - .putExtra("receiver", new ProgressReceiver(activity, new Handler())); - - activity.startService(intent); - } - } -} diff --git a/app/src/main/java/org/cuberite/android/helpers/StateHelper.java b/app/src/main/java/org/cuberite/android/helpers/StateHelper.java deleted file mode 100644 index db5af47..0000000 --- a/app/src/main/java/org/cuberite/android/helpers/StateHelper.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.cuberite.android.helpers; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import java.io.File; - -import static android.content.Context.MODE_PRIVATE; - -public class StateHelper { - public enum State { - NEED_DOWNLOAD_SERVER, - NEED_DOWNLOAD_BINARY, - NEED_DOWNLOAD_BOTH, - PICK_FILE_BINARY, - PICK_FILE_SERVER, - RUNNING, - READY - } - - public static boolean isCuberiteInstalled(Context context) { - State state = getState(context); - return ( - state != State.NEED_DOWNLOAD_BINARY - && state != State.NEED_DOWNLOAD_SERVER - && state != State.NEED_DOWNLOAD_BOTH - ); - } - - public static State getState(Context context) { - // Logging tag - String LOG = "Cuberite/State"; - - final SharedPreferences preferences = context.getSharedPreferences(context.getPackageName(), MODE_PRIVATE); - boolean hasBinary = false; - boolean hasServer = false; - - if (new File(context.getFilesDir().getAbsolutePath() + "/" + CuberiteHelper.getExecutableName()).exists()) { - hasBinary = true; - } - - if (new File(preferences.getString("cuberiteLocation", "")).exists()) { - hasServer = true; - } - - // Update state - State state = State.READY; - - if (CuberiteHelper.isCuberiteRunning(context)) { - state = State.RUNNING; - } else if (!hasBinary && !hasServer) { - state = State.NEED_DOWNLOAD_BOTH; - } else if (!hasBinary) { - state = State.NEED_DOWNLOAD_BINARY; - } else if (!hasServer) { - state = State.NEED_DOWNLOAD_SERVER; - } - - Log.d(LOG, "Getting State: " + state); - return state; - } -} diff --git a/app/src/main/java/org/cuberite/android/preferences/MaterialListPreference.java b/app/src/main/java/org/cuberite/android/preferences/MaterialListPreference.java deleted file mode 100644 index f0d8c9a..0000000 --- a/app/src/main/java/org/cuberite/android/preferences/MaterialListPreference.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.cuberite.android.preferences; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -public class MaterialListPreference extends ListPreference { - public MaterialListPreference(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onClick() { - final Context context = getContext(); - new MaterialAlertDialogBuilder(context) - .setTitle(getTitle()) - .setCancelable(true) - .setSingleChoiceItems( - getEntries(), - findIndexOfValue(getValue()), - (dialog, index) -> { - if (callChangeListener(getEntryValues()[index].toString())) { - setValueIndex(index); - } - dialog.dismiss(); - }) - .setNegativeButton( - getNegativeButtonText(), - (dialog, button) -> dialog.dismiss()) - .show(); - } -} diff --git a/app/src/main/java/org/cuberite/android/receivers/ProgressReceiver.java b/app/src/main/java/org/cuberite/android/receivers/ProgressReceiver.java deleted file mode 100644 index 9bb25da..0000000 --- a/app/src/main/java/org/cuberite/android/receivers/ProgressReceiver.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.cuberite.android.receivers; - -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.ResultReceiver; -import android.view.View; - -import androidx.appcompat.app.AlertDialog; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.progressindicator.LinearProgressIndicator; - -import org.cuberite.android.R; - -public class ProgressReceiver extends ResultReceiver { - public static final int PROGRESS_START = 0; - public static final int PROGRESS_NEW_DATA = 1; - public static final int PROGRESS_END = 2; - - private final Context cont; - - private AlertDialog progressDialog; - private LinearProgressIndicator progressBar; - - public ProgressReceiver(Context context, Handler handler) { - super(handler); - cont = context; - } - - private void createDialog(String title) { - final View layout = View.inflate(cont, R.layout.dialog_progress, null); - progressBar = ((LinearProgressIndicator) layout.findViewById(R.id.progressBar)); - progressDialog = new MaterialAlertDialogBuilder(cont) - .setTitle(title) - .setView(layout) - .setCancelable(false) - .create(); - } - - @Override - public void onReceiveResult(int resultCode, Bundle resultData) { - super.onReceiveResult(resultCode, resultData); - switch (resultCode) { - case PROGRESS_START -> { - String title = resultData.getString("title"); - if (progressDialog == null) { - createDialog(title); - } else { - progressDialog.setTitle(title); - } - progressBar.setIndeterminate(true); - progressDialog.show(); - } - case PROGRESS_NEW_DATA -> { - int progress = resultData.getInt("progress"); - int max = resultData.getInt("max"); - progressBar.setIndeterminate(false); - progressBar.setProgressCompat(progress, true); - progressBar.setMax(max); - } - case PROGRESS_END -> { - progressDialog.dismiss(); - progressDialog = null; - } - } - } -} diff --git a/app/src/main/java/org/cuberite/android/receivers/StartupReceiver.java b/app/src/main/java/org/cuberite/android/receivers/StartupReceiver.java deleted file mode 100644 index c999f82..0000000 --- a/app/src/main/java/org/cuberite/android/receivers/StartupReceiver.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cuberite.android.receivers; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; - -import org.cuberite.android.helpers.CuberiteHelper; -import org.cuberite.android.helpers.StateHelper; - -import static android.content.Context.MODE_PRIVATE; - -public class StartupReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - final SharedPreferences preferences = context.getSharedPreferences(context.getPackageName(), MODE_PRIVATE); - - if (preferences.getBoolean("startOnBoot", false) - && StateHelper.isCuberiteInstalled(context)) { - CuberiteHelper.startCuberite(context); - } - } - } -} diff --git a/app/src/main/java/org/cuberite/android/services/CuberiteService.java b/app/src/main/java/org/cuberite/android/services/CuberiteService.java deleted file mode 100644 index 8e7070c..0000000 --- a/app/src/main/java/org/cuberite/android/services/CuberiteService.java +++ /dev/null @@ -1,217 +0,0 @@ -package org.cuberite.android.services; - -import android.app.IntentService; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import android.content.SharedPreferences; -import android.net.NetworkInfo; -import android.net.wifi.WifiManager; -import android.os.Build; -import android.util.Log; - -import org.cuberite.android.MainActivity; -import org.cuberite.android.R; -import org.cuberite.android.helpers.CuberiteHelper; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.NoSuchElementException; -import java.util.Scanner; - -public class CuberiteService extends IntentService { - // Logging tag - private static final String LOG = "Cuberite/ServerService"; - - private NotificationCompat.Builder notification; - private Process process; - private OutputStream cuberiteSTDIN; - - public CuberiteService() { - super("CuberiteService"); - } - - - // Notification-related methods - - private void createNotification() { - final String channelId = "cuberiteservice"; - final int icon = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ? R.drawable.ic_notification : R.mipmap.ic_launcher; - final CharSequence text = getText(R.string.notification_cuberite_running); - final String ip = CuberiteHelper.getIpAddress(getApplicationContext()); - - final int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ? PendingIntent.FLAG_IMMUTABLE : 0; - final Intent notificationIntent = new Intent(this, MainActivity.class); - final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags); - - notification = new NotificationCompat.Builder(this, channelId) - .setSmallIcon(icon) - .setTicker(text) - .setContentTitle(text) - .setContentText(ip) - .setContentIntent(contentIntent) - .setOnlyAlertOnce(true) - .setOngoing(true) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE); - - startForeground(1, notification.build()); - } - - - // Process-related methods - - private void startProcess() throws IOException { - final SharedPreferences preferences = getApplicationContext().getSharedPreferences(this.getPackageName(), MODE_PRIVATE); - final String executableName = CuberiteHelper.getExecutableName(); - final String location = preferences.getString("cuberiteLocation", ""); - - // Clear previous output - CuberiteHelper.resetConsoleOutput(); - - // Make sure we can execute the binary - new File(this.getFilesDir(), executableName).setExecutable(true, true); - - // Initiate ProcessBuilder with the command at the given location - ProcessBuilder processBuilder = new ProcessBuilder(this.getFilesDir() + "/" + executableName, "--no-output-buffering"); - processBuilder.directory(new File(location)); - processBuilder.redirectErrorStream(true); - - CuberiteHelper.addConsoleOutput(getApplicationContext(), "Info: Cuberite is starting..."); - Log.d(LOG, "Starting process..."); - process = processBuilder.start(); - cuberiteSTDIN = process.getOutputStream(); - } - - private void updateOutput() { - Log.d(LOG, "Starting logging..."); - - final Scanner processScanner = new Scanner(process.getInputStream()); - String line; - - try { - while ((line = processScanner.nextLine()) != null) { - Log.i(LOG, line); - CuberiteHelper.addConsoleOutput(getApplicationContext(), line); - } - } catch (NoSuchElementException e) { - // Do nothing. Workaround for issues in older Android versions. - } - - processScanner.close(); - } - - - // Broadcast receivers - - private final BroadcastReceiver executeCommand = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String command = intent.getStringExtra("message"); - try { - cuberiteSTDIN.write((command + "\n").getBytes()); - cuberiteSTDIN.flush(); - } catch (Exception e) { - Log.e(LOG, "An error occurred when writing " + command + " to the STDIN", e); - } - } - }; - - private final BroadcastReceiver updateIp = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { - final NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - - if (NetworkInfo.State.CONNECTED.equals(info.getState()) - || NetworkInfo.State.DISCONNECTED.equals(info.getState())) { - Log.d(LOG, "Updating notification IP due to network change"); - final String ip = CuberiteHelper.getIpAddress(context); - notification.setContentText(ip); - - final NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - notificationManager.notify(1, notification.build()); - } - } - } - }; - - private final BroadcastReceiver stop = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - try { - cuberiteSTDIN.write(("stop\n").getBytes()); - cuberiteSTDIN.flush(); - } catch (Exception e) { - Log.e(LOG, "An error occurred when writing stop to the STDIN", e); - } - } - }; - - private final BroadcastReceiver kill = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - process.destroy(); - } - }; - - - // Service startup and cleanup - - @Override - protected void onHandleIntent(Intent intent) { - Log.d(LOG, "Starting service..."); - - try { - // Create and show notification about Cuberite running - createNotification(); - - // Start the Cuberite process - startProcess(); - - // Update notification IP if network changes - IntentFilter intentFilter = new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION); - registerReceiver(updateIp, intentFilter); - - // Communication with the activity - LocalBroadcastManager.getInstance(this).registerReceiver(executeCommand, new IntentFilter("executeCommand")); - LocalBroadcastManager.getInstance(this).registerReceiver(stop, new IntentFilter("stop")); - LocalBroadcastManager.getInstance(this).registerReceiver(kill, new IntentFilter("kill")); - - // Log to console - final long logTimeStart = System.currentTimeMillis(); - updateOutput(); - - // Logic waits here until Cuberite has stopped. Everything after that is cleanup for the next run - - final long logTimeEnd = System.currentTimeMillis(); - if ((logTimeEnd - logTimeStart) < 100) { - LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("showStartupError")); - } - - // Shutdown - unregisterReceiver(updateIp); - - LocalBroadcastManager.getInstance(this).unregisterReceiver(executeCommand); - LocalBroadcastManager.getInstance(this).unregisterReceiver(stop); - LocalBroadcastManager.getInstance(this).unregisterReceiver(kill); - - cuberiteSTDIN.close(); - } catch (Exception e) { - Log.e(LOG, "An error occurred when starting Cuberite", e); - - // Send error to user - LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("showStartupError")); - } - - stopSelf(); - LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent("CuberiteService.callback")); - } -} diff --git a/app/src/main/java/org/cuberite/android/services/InstallService.java b/app/src/main/java/org/cuberite/android/services/InstallService.java deleted file mode 100644 index 2b4675b..0000000 --- a/app/src/main/java/org/cuberite/android/services/InstallService.java +++ /dev/null @@ -1,335 +0,0 @@ -package org.cuberite.android.services; - -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.PowerManager; - -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import android.os.ResultReceiver; -import android.util.Log; - -import org.cuberite.android.helpers.CuberiteHelper; -import org.cuberite.android.helpers.StateHelper.State; -import org.cuberite.android.receivers.ProgressReceiver; -import org.cuberite.android.R; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.MessageDigest; -import java.util.Scanner; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -public class InstallService extends IntentService { - // Logging tag - private static final String LOG = "Cuberite/InstallService"; - - private ResultReceiver receiver; - - public InstallService() { - super("InstallService"); - } - - - // Wakelock - - private PowerManager.WakeLock acquireWakelock() { - Log.d(LOG, "Acquiring wakeLock"); - - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - final PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); - wakeLock.acquire(300000); // 5 min timeout - - return wakeLock; - } - - - // Download verification - - private String downloadVerify(String url, File targetLocation, int retryCount) { - final String zipFileError = download(url, targetLocation); - if (zipFileError != null) { - return zipFileError; - } - - // Verifying file - final String shaError = download(url + ".sha1", new File(targetLocation + ".sha1")); - if (shaError != null) { - return shaError; - } - - try { - final String generatedSha = generateSha1(targetLocation); - final String downloadedSha = new Scanner( - new File(targetLocation + ".sha1") - ) - .useDelimiter("\\Z") - .next() - .split(" ", 2)[0]; - new File(targetLocation + ".sha1").delete(); - - if (!downloadedSha.equals(generatedSha)) { - Log.d(LOG, "SHA-1 check didn't pass"); - - if (retryCount > 0) { - // Retry if verification failed - return downloadVerify(url, targetLocation, retryCount - 1); - } - - return getString(R.string.status_shasum_error); - } - - Log.d(LOG, "SHA-1 check passed successfully with checksum " + generatedSha); - } catch (FileNotFoundException e) { - Log.e(LOG, "Something went wrong while generating checksum", e); - return getString(R.string.status_shasum_error); - } - - return null; - } - - private String generateSha1(File targetLocation) { - try { - MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - InputStream input = new FileInputStream(targetLocation); - byte[] buffer = new byte[8192]; - int len = input.read(buffer); - - while (len != -1) { - sha1.update(buffer, 0, len); - len = input.read(buffer); - } - byte[] shaSum = sha1.digest(); - char[] charset = "0123456789ABCDEF".toCharArray(); - char[] hexResult = new char[shaSum.length * 2]; - for (int j = 0; j < shaSum.length; j++) { - int v = shaSum[j] & 0xFF; - hexResult[j * 2] = charset[v >>> 4]; - hexResult[j * 2 + 1] = charset[v & 0x0F]; - } - return new String(hexResult).toLowerCase(); - } catch (Exception e) { - return e.toString(); - } - } - - - // Download - - private String download(String stringUrl, File targetLocation) { - final PowerManager.WakeLock wakeLock = acquireWakelock(); - - String result = null; - - InputStream inputStream = null; - OutputStream outputStream = null; - HttpURLConnection connection = null; - - Bundle bundleInit = new Bundle(); - bundleInit.putString("title", getString(R.string.status_downloading_cuberite)); - receiver.send(ProgressReceiver.PROGRESS_START, bundleInit); - - install: try { - Log.d(LOG, "Started downloading " + stringUrl); - Log.d(LOG, "Downloading to " + targetLocation); - - URL url = new URL(stringUrl); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(10000); // 10 secs - connection.connect(); - - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - String error = "Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage(); - Log.e(LOG, error); - result = error; - break install; - } - - int length = connection.getContentLength(); - inputStream = connection.getInputStream(); - outputStream = new FileOutputStream(targetLocation); - - byte[] data = new byte[4096]; - long total = 0; - int count; - while ((count = inputStream.read(data)) != -1) { - total += count; - if (length > 0) { // only if total length is known - Bundle bundleProg = new Bundle(); - bundleProg.putInt("progress", (int) total); - bundleProg.putInt("max", length); - receiver.send(ProgressReceiver.PROGRESS_NEW_DATA, bundleProg); - } - outputStream.write(data, 0, count); - } - Log.d(LOG, "Finished downloading"); - } catch (Exception e) { - result = e.getMessage(); - Log.e(LOG, "An error occurred when downloading a zip", e); - } finally { - try { - if (outputStream != null) { - outputStream.close(); - } - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException ignored) {} - - if (connection != null) { - connection.disconnect(); - } - } - - if (result != null) { - receiver.send(ProgressReceiver.PROGRESS_END, null); - } - - Log.d(LOG, "Releasing wakeLock"); - wakeLock.release(); - return result; - } - - - // Unzip - - private String unzip(Uri fileUri, File targetLocation) { - String result = getString(R.string.status_install_success); - - Log.i(LOG, "Unzipping " + fileUri + " to " + targetLocation); - - final PowerManager.WakeLock wakeLock = acquireWakelock(); - - if (!targetLocation.exists()) { - targetLocation.mkdir(); - } - - // Create a .nomedia file in the server directory to prevent images from showing in gallery - createNoMediaFile(targetLocation); - - Bundle bundleInit = new Bundle(); - bundleInit.putString("title", getString(R.string.status_installing_cuberite)); - receiver.send(ProgressReceiver.PROGRESS_START, bundleInit); - - try { - unzipStream(fileUri, targetLocation); - } catch (IOException e) { - result = getString(R.string.status_unzip_error); - Log.e(LOG, "An error occurred while installing Cuberite", e); - } - - receiver.send(ProgressReceiver.PROGRESS_END, null); - - Log.d(LOG, "Releasing wakeLock"); - wakeLock.release(); - - return result; - } - - private void unzipStream(Uri fileUri, File targetLocation) throws IOException { - InputStream inputStream = getContentResolver().openInputStream(fileUri); - ZipInputStream zipInputStream = new ZipInputStream(inputStream); - ZipEntry zipEntry; - - while ((zipEntry = zipInputStream.getNextEntry()) != null) { - if (zipEntry.isDirectory()) { - new File(targetLocation, zipEntry.getName()).mkdir(); - } else { - FileOutputStream outputStream = new FileOutputStream(targetLocation + "/" + zipEntry.getName()); - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - byte[] buffer = new byte[1024]; - int read; - while ((read = zipInputStream.read(buffer)) != -1) { - bufferedOutputStream.write(buffer, 0, read); - } - zipInputStream.closeEntry(); - bufferedOutputStream.close(); - outputStream.close(); - } - - } - zipInputStream.close(); - } - - private void createNoMediaFile(File targetFolder) { - final File noMedia = new File(targetFolder, ".nomedia"); - try { - noMedia.createNewFile(); - } catch (IOException e) { - Log.e(LOG, "Something went wrong while creating the .nomedia file", e); - } - } - - - // Service handler - - @Override - protected void onHandleIntent(Intent intent) { - State state = (State) intent.getSerializableExtra("state"); - String result; - - if ( - (state == State.NEED_DOWNLOAD_BINARY - || state == State.NEED_DOWNLOAD_BOTH - || state == State.PICK_FILE_BINARY) - && CuberiteHelper.isCuberiteRunning(getApplicationContext()) - ) { - result = getString(R.string.status_update_binary_error); - } else if ("unzip".equals(intent.getAction())) { - final Uri uri = intent.getParcelableExtra("uri"); - final File targetFolder = new File( - state == State.PICK_FILE_BINARY ? this.getFilesDir().getAbsolutePath() : intent.getStringExtra("targetFolder") - ); - receiver = intent.getParcelableExtra("receiver"); - result = unzip(uri, targetFolder); - } else { - final String downloadHost = intent.getStringExtra("downloadHost"); - final String abi = CuberiteHelper.getPreferredABI(); - - final String targetFileName = (state == State.NEED_DOWNLOAD_BINARY || state == State.NEED_DOWNLOAD_BOTH ? abi : "server") + ".zip"; - final String downloadUrl = downloadHost + targetFileName; - final File tempZip = new File(this.getCacheDir(), targetFileName); // Zip files are temporary - final File targetFolder = new File( - state == State.NEED_DOWNLOAD_BINARY || state == State.NEED_DOWNLOAD_BOTH ? this.getFilesDir().getAbsolutePath() : intent.getStringExtra("targetFolder") - ); - receiver = intent.getParcelableExtra("receiver"); - - // Download - Log.i(LOG, "Downloading " + state); - - final int retryCount = 1; - result = downloadVerify(downloadUrl, tempZip, retryCount); - - if (result == null) { - result = unzip(Uri.fromFile(tempZip), targetFolder); - - if (!tempZip.delete()) { - Log.w(LOG, "Failed to delete downloaded zip file"); - } - } - - if (state == State.NEED_DOWNLOAD_BOTH) { - intent.putExtra("state", State.NEED_DOWNLOAD_SERVER); - onHandleIntent(intent); - } - } - - stopSelf(); - LocalBroadcastManager.getInstance(this).sendBroadcast( - new Intent("InstallService.callback") - .putExtra("result", result) - ); - } -} diff --git a/app/src/main/kotlin/org/cuberite/android/MainActivity.kt b/app/src/main/kotlin/org/cuberite/android/MainActivity.kt new file mode 100644 index 0000000..c52874f --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/MainActivity.kt @@ -0,0 +1,139 @@ +package org.cuberite.android + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.elevation.SurfaceColors +import com.google.android.material.navigation.NavigationBarView +import com.google.android.material.snackbar.Snackbar +import org.cuberite.android.fragments.ConsoleFragment +import org.cuberite.android.fragments.ControlFragment +import org.cuberite.android.fragments.SettingsFragment + +class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { + // Logging tag + private val log = "Cuberite/MainActivity" + private var permissionPopup: AlertDialog? = null + private lateinit var preferences: SharedPreferences + private lateinit var privateDir: String + private lateinit var publicDir: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.container) + if (savedInstanceState == null) { + loadFragment(ControlFragment()) + } + + // Set colors + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this) + } + + // Set navigation bar listener + val navigation: BottomNavigationView = findViewById(R.id.bottom_navigation) + navigation.setOnItemSelectedListener(this) + + // Initialize settings + preferences = getSharedPreferences(this.packageName, MODE_PRIVATE) + privateDir = this.filesDir.absolutePath + publicDir = Environment.getExternalStorageDirectory().absolutePath + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + var fragment: Fragment? = null + when (item.itemId) { + R.id.item_control -> { + fragment = ControlFragment() + } + R.id.item_console -> { + fragment = ConsoleFragment() + } + R.id.item_settings -> { + fragment = SettingsFragment() + } + } + return loadFragment(fragment) + } + + private fun loadFragment(fragment: Fragment?): Boolean { + fragment?.let { + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit() + return true + } + return false + } + + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + Log.i(log, "Got permissions, using public directory") + preferences.edit().putString("cuberiteLocation", "$publicDir/cuberite-server").apply() + } else { + Log.i(log, "Permissions denied, boo, using private directory") + preferences.edit().putString("cuberiteLocation", "$privateDir/cuberite-server").apply() + } + } + + private fun showPermissionPopup() { + permissionPopup = MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.status_permissions_needed)) + .setMessage(R.string.message_externalstorage_permission) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + Log.d(log, "Requesting permissions for external storage") + permissionPopup = null + requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + .create() + permissionPopup!!.show() + } + + private fun checkPermissions() { + val location = preferences.getString("cuberiteLocation", "") + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + // User is running Android 6 or above, show permission popup on first run + // or if user granted permission and later denied it + if (location!!.isEmpty() || location.startsWith(publicDir)) { + showPermissionPopup() + } + } else if (location!!.isEmpty() || location.startsWith(privateDir)) { + val editor = preferences.edit() + editor.putString("cuberiteLocation", "$publicDir/cuberite-server") + editor.apply() + } + } + + public override fun onPause() { + super.onPause() + permissionPopup?.let { + permissionPopup!!.dismiss() + permissionPopup = null + } + } + + public override fun onResume() { + super.onResume() + checkPermissions() + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/MainApplication.kt b/app/src/main/kotlin/org/cuberite/android/MainApplication.kt new file mode 100644 index 0000000..d52c4c2 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/MainApplication.kt @@ -0,0 +1,40 @@ +package org.cuberite.android + +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import com.google.android.material.color.DynamicColors + +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + + // Application theme + val preferences = getSharedPreferences(this.packageName, MODE_PRIVATE) + AppCompatDelegate.setDefaultNightMode(preferences.getInt("defaultTheme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)) + DynamicColors.applyToActivitiesIfAvailable(this) + + // Notification channel + createNotificationChannel() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + val channelId = "cuberiteservice" + val name = getString(R.string.app_name) + val channel = NotificationChannel( + channelId, + name, + NotificationManager.IMPORTANCE_HIGH + ) + channel.setSound(null, null) + channel.setVibrationPattern(longArrayOf(0)) + channel.enableVibration(true) + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/cuberite/android/fragments/ConsoleFragment.kt b/app/src/main/kotlin/org/cuberite/android/fragments/ConsoleFragment.kt new file mode 100644 index 0000000..0edd04e --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/fragments/ConsoleFragment.kt @@ -0,0 +1,127 @@ +package org.cuberite.android.fragments + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.util.Log +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ScrollView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.android.material.color.MaterialColors +import com.google.android.material.textfield.TextInputLayout +import org.cuberite.android.R +import org.cuberite.android.helpers.CuberiteHelper + +class ConsoleFragment : Fragment() { + private lateinit var logView: TextView + private lateinit var inputLine: EditText + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_console, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + logView = view.findViewById(R.id.logView) + inputLine = view.findViewById(R.id.inputLine) + inputLine.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + val command = inputLine.getText().toString() + sendExecuteCommand(command) + inputLine.setText("") + // return true makes sure the keyboard doesn't close + return@setOnEditorActionListener true + } + false + } + val textInputLayout = view.findViewById(R.id.inputWrapper) + textInputLayout.setEndIconOnClickListener { + val command = inputLine.getText().toString() + sendExecuteCommand(command) + inputLine.setText("") + } + } + + private fun sendExecuteCommand(command: String) { + if (command.isNotEmpty() + && CuberiteHelper.isCuberiteRunning(requireActivity())) { + // Logging tag + val log = "Cuberite/Console" + Log.d(log, "Executing $command") + val intent = Intent("executeCommand") + intent.putExtra("message", command) + LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent) + } + } + + // Broadcast receivers + private val updateLog: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val scrollView = logView.parent as ScrollView + val shouldScroll = logView.bottom - (scrollView.height + scrollView.scrollY) <= 0 + val output = CuberiteHelper.getConsoleOutput() + val formattedOutput = SpannableStringBuilder() + for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + if (line.isEmpty()) { + continue + } + if (formattedOutput.isNotEmpty()) { + // Line break + formattedOutput.append("\n") + } + var color = -1 + var processedLine = line + if (line.lowercase().startsWith("log: ")) { + processedLine = line.replaceFirst("(?i)log: ".toRegex(), "") + } else if (line.lowercase().startsWith("info: ")) { + processedLine = line.replaceFirst("(?i)info: ".toRegex(), "") + color = com.google.android.material.R.attr.colorTertiary + } else if (line.lowercase().startsWith("warning: ")) { + processedLine = line.replaceFirst("(?i)warning: ".toRegex(), "") + color = com.google.android.material.R.attr.colorError + } else if (line.lowercase().startsWith("error: ")) { + processedLine = line.replaceFirst("(?i)error: ".toRegex(), "") + color = com.google.android.material.R.attr.colorOnErrorContainer + } + val logLine = SpannableStringBuilder(processedLine) + if (color >= 0) { + val start = 0 + val end = logLine.length + color = MaterialColors.getColor(requireContext(), color, Color.BLACK) + logLine.setSpan(ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + formattedOutput.append(logLine) + } + logView.text = formattedOutput + if (shouldScroll) { + scrollView.post { + scrollView.fullScroll(ScrollView.FOCUS_DOWN) + inputLine.requestFocus() + } + } + } + } + + override fun onPause() { + super.onPause() + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(updateLog) + } + + override fun onResume() { + super.onResume() + LocalBroadcastManager.getInstance(requireContext()).registerReceiver(updateLog, IntentFilter("updateLog")) + LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(Intent("updateLog")) + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/fragments/ControlFragment.kt b/app/src/main/kotlin/org/cuberite/android/fragments/ControlFragment.kt new file mode 100644 index 0000000..c4cf340 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/fragments/ControlFragment.kt @@ -0,0 +1,147 @@ +package org.cuberite.android.fragments + +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.android.material.color.MaterialColors +import com.google.android.material.snackbar.Snackbar +import org.cuberite.android.R +import org.cuberite.android.helpers.CuberiteHelper +import org.cuberite.android.helpers.InstallHelper +import org.cuberite.android.helpers.StateHelper + +class ControlFragment : Fragment() { + // Logging tag + private val log = "Cuberite/Control" + private var mainButtonColor = 0 + private lateinit var mainButton: Button + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_control, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + mainButton = view.findViewById(R.id.mainButton) + mainButtonColor = MaterialColors.getColor(mainButton, com.google.android.material.R.attr.colorSurface) + } + + private fun animateColorChange(button: Button, colorFrom: Int, colorTo: Int) { + Log.d(log, "Changing color from " + Integer.toHexString(colorFrom) + " to " + Integer.toHexString(colorTo)) + val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo) + colorAnimation.setDuration(300) + colorAnimation.addUpdateListener { animator: ValueAnimator -> button.setBackgroundColor(animator.getAnimatedValue() as Int) } + colorAnimation.start() + mainButtonColor = colorTo + } + + private fun setInstallButton(state: StateHelper.State?) { + val colorTo = MaterialColors.getColor(mainButton, com.google.android.material.R.attr.colorPrimary) + animateColorChange(mainButton, mainButtonColor, colorTo) + mainButton.text = getText(R.string.do_install_cuberite) + mainButton.setOnClickListener { + LocalBroadcastManager.getInstance(requireContext()).registerReceiver( + installServiceCallback, + IntentFilter("InstallService.callback") + ) + InstallHelper.installCuberiteDownload(requireActivity(), state) + } + } + + private fun setStartButton() { + val colorTo = MaterialColors.getColor(mainButton, com.google.android.material.R.attr.colorPrimary) + animateColorChange(mainButton, mainButtonColor, colorTo) + mainButton.text = getText(R.string.do_start_cuberite) + mainButton.setOnClickListener { + LocalBroadcastManager.getInstance(requireContext()).registerReceiver( + showStartupError, + IntentFilter("showStartupError") + ) + CuberiteHelper.startCuberite(requireContext()) + setStopButton() + } + } + + private fun setStopButton() { + val colorTo = MaterialColors.getColor(mainButton, com.google.android.material.R.attr.colorTertiary) + animateColorChange(mainButton, mainButtonColor, colorTo) + mainButton.text = getText(R.string.do_stop_cuberite) + mainButton.setOnClickListener { + CuberiteHelper.stopCuberite(requireContext()) + setKillButton() + } + } + + private fun setKillButton() { + val colorTo = MaterialColors.getColor(mainButton, com.google.android.material.R.attr.colorError) + animateColorChange(mainButton, mainButtonColor, colorTo) + mainButton.text = getText(R.string.do_kill_cuberite) + mainButton.setOnClickListener { CuberiteHelper.killCuberite(requireContext()) } + } + + private fun updateControlButton() { + when (val state = StateHelper.getState(requireContext())) { + StateHelper.State.RUNNING -> { + setStopButton() + } + StateHelper.State.READY -> { + setStartButton() + } + else -> { + setInstallButton(state) + } + } + } + + // Broadcast receivers + private val cuberiteServiceCallback: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + updateControlButton() + } + } + private val installServiceCallback: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + LocalBroadcastManager.getInstance(context).unregisterReceiver(this) + val result = intent.getStringExtra("result") + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), result!!, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + updateControlButton() + } + } + private val showStartupError: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + LocalBroadcastManager.getInstance(context).unregisterReceiver(this) + Log.d(log, "Cuberite exited on process") + val message = String.format( + getString(R.string.status_failed_start), + CuberiteHelper.preferredABI + ) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + } + } + + // Register/unregister receivers and update button state + override fun onPause() { + super.onPause() + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(cuberiteServiceCallback) + } + + override fun onResume() { + super.onResume() + LocalBroadcastManager.getInstance(requireContext()).registerReceiver(cuberiteServiceCallback, IntentFilter("CuberiteService.callback")) + updateControlButton() + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/fragments/SettingsFragment.kt b/app/src/main/kotlin/org/cuberite/android/fragments/SettingsFragment.kt new file mode 100644 index 0000000..6e565c2 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/fragments/SettingsFragment.kt @@ -0,0 +1,384 @@ +package org.cuberite.android.fragments + +import android.Manifest +import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.View +import android.widget.EditText +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import org.cuberite.android.BuildConfig +import org.cuberite.android.R +import org.cuberite.android.helpers.CuberiteHelper +import org.cuberite.android.helpers.InstallHelper +import org.cuberite.android.helpers.StateHelper +import org.ini4j.Config +import org.ini4j.Ini +import java.io.File +import java.io.IOException + +class SettingsFragment : PreferenceFragmentCompat() { + // Logging tag + private val log = "Cuberite/Settings" + + override fun onCreatePreferences(bundle: Bundle?, s: String?) { + addPreferencesFromResource(R.xml.preferences) + val preferences = requireContext().getSharedPreferences(requireContext().packageName, Context.MODE_PRIVATE) + + // Ini4j config + val config = Config.getGlobal() + config.isEscape = false + config.isStrictOperator = true + + // Initialize + initializeThemeSettings(preferences) + initializeStartupSettings(preferences) + initializeSDCardSettings(preferences) + initializeWebadminSettings(preferences) + initializeInstallSettings() + initializeInfoSettings(preferences) + } + + // Theme-related methods + private fun initializeThemeSettings(preferences: SharedPreferences) { + val getCurrentTheme = preferences.getInt("defaultTheme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + val theme = findPreference("theme") + theme!!.dialogTitle = getString(R.string.settings_theme_choose) + theme.entries = arrayOf( + getString(R.string.settings_theme_light), + getString(R.string.settings_theme_dark), + getString(R.string.settings_theme_auto) + ) + theme.entryValues = arrayOf("light", "dark", "auto") + when (getCurrentTheme) { + AppCompatDelegate.MODE_NIGHT_NO -> theme.setValue("light") + AppCompatDelegate.MODE_NIGHT_YES -> theme.setValue("dark") + else -> theme.setValue("auto") + } + theme.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + val newTheme = when (newValue.toString()) { + "light" -> AppCompatDelegate.MODE_NIGHT_NO + "dark" -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + AppCompatDelegate.setDefaultNightMode(newTheme) + val editor = preferences.edit() + editor.putInt("defaultTheme", newTheme) + editor.apply() + true + } + } + + // Startup-related methods + private fun initializeStartupSettings(preferences: SharedPreferences) { + val startupToggle = findPreference("startupToggle") + startupToggle!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + val editor = preferences.edit() + editor.putBoolean("startOnBoot", newValue as Boolean) + editor.apply() + true + } + } + + // SD Card-related methods + private fun initializeSDCardSettings(preferences: SharedPreferences) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return + } + val privateDir = requireActivity().filesDir.absolutePath + val publicDir = Environment.getExternalStorageDirectory().absolutePath + val location = preferences.getString("cuberiteLocation", "") + val isSDAvailable = (requireContext().getExternalFilesDirs(null).size > 1) + val isSDEnabled = !(location!!.startsWith(publicDir) || location.startsWith(privateDir)) + val toggleSD = findPreference("saveToSDToggle") + if (!(isSDAvailable || isSDEnabled)) { + return + } + Log.d(log, "SD Card found or location set, showing preference") + toggleSD!!.isVisible = true + toggleSD.setChecked(isSDEnabled) + toggleSD.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + if (CuberiteHelper.isCuberiteRunning(requireContext())) { + val message = getString(R.string.settings_sd_card_running) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + return@OnPreferenceChangeListener false + } + val isSDAvailableInner = (requireContext().getExternalFilesDirs(null).size > 1) + var newLocation = publicDir + if (newValue as Boolean && isSDAvailableInner) { + // SD dir + newLocation = requireContext().getExternalFilesDirs(null)[1].absolutePath + } else { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + // Private dir + newLocation = requireContext().filesDir.absolutePath + } + toggleSD.isVisible = isSDAvailableInner + } + val editor = preferences.edit() + editor.putString("cuberiteLocation", "$newLocation/cuberite-server") + editor.apply() + true + } + } + + // Webadmin-related methods + private fun initializeWebadminSettings(preferences: SharedPreferences) { + val webadminFile = getWebadminFile(preferences) + val url = getWebadminUrl(webadminFile) + url?.let { + val webadminDescription = findPreference("webadminDescription") + webadminDescription!!.setSummary(""" + ${webadminDescription.getSummary().toString()} + + URL: $url + """.trimIndent()) + } + val webadminOpen = findPreference("webadminOpen") + webadminOpen!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (!CuberiteHelper.isCuberiteRunning(requireContext())) { + val message = getString(R.string.settings_webadmin_not_running) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + return@OnPreferenceClickListener true + } + val webadminFileInner = getWebadminFile(preferences) + val urlInner = getWebadminUrl(webadminFileInner) + urlInner?.let { + Log.d(log, "Opening Webadmin on $urlInner") + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlInner)) + startActivity(browserIntent) + } + true + } + val webadminLogin = findPreference("webadminLogin") + webadminLogin!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + try { + val webadminFileInner = getWebadminFile(preferences) + val ini = createWebadminIni(webadminFileInner) + ini.put("WebAdmin", "Enabled", 1) + showWebadminCredentialPopup(webadminFileInner, ini) + } catch (e: IOException) { + Log.e(log, "Something went wrong while opening the ini file", e) + val message = getString(R.string.settings_webadmin_error) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + } + true + } + } + + @Throws(IOException::class) + private fun createWebadminIni(webadminFile: File): Ini { + val ini: Ini + if (!webadminFile.exists()) { + ini = Ini() + ini.put("WebAdmin", "Ports", 8080) + ini.put("WebAdmin", "Enabled", 1) + ini.store(webadminFile) + } else { + ini = Ini(webadminFile) + } + return ini + } + + private fun getWebadminFile(preferences: SharedPreferences): File { + val cuberiteDir = File(preferences.getString("cuberiteLocation", "")!!) + return File(cuberiteDir, "webadmin.ini") + } + + private fun getWebadminUrl(webadminFile: File): String? { + var url: String? = null + try { + val ini = createWebadminIni(webadminFile) + val ip = CuberiteHelper.getIpAddress(requireContext()) + val port: Int = try { + ini["WebAdmin", "Ports"].toInt() + } catch (e: NumberFormatException) { + ini.put("WebAdmin", "Ports", 8080) + ini.store(webadminFile) + ini["WebAdmin", "Ports"].toInt() + } + url = "http://$ip:$port" + } catch (e: IOException) { + Log.e(log, "Something went wrong while opening the ini file", e) + } + return url + } + + private fun showWebadminCredentialPopup(webadminFile: File, ini: Ini) { + var username = "" + var password = "" + for (sectionName in ini.keys) { + if (sectionName.startsWith("User:")) { + username = sectionName.substring(5) + password = ini[sectionName, "Password"] + } + } + val oldUsername = username + val layout = View.inflate(requireContext(), R.layout.dialog_webadmin_credentials, null) + (layout.findViewById(R.id.webadminUsername) as EditText).setText(username) + (layout.findViewById(R.id.webadminPassword) as EditText).setText(password) + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setView(layout) + .setTitle(R.string.settings_webadmin_login) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + val newUsername = (layout.findViewById(R.id.webadminUsername) as EditText).getText().toString() + val newPassword = (layout.findViewById(R.id.webadminPassword) as EditText).getText().toString() + ini.remove("User:$oldUsername") + ini.put("User:$newUsername", "Password", newPassword) + try { + ini.store(webadminFile) + val message = getString(R.string.settings_webadmin_success) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + } catch (e: IOException) { + Log.e(log, "Something went wrong while saving the ini file", e) + val message = getString(R.string.settings_webadmin_error) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + } + } + .setNegativeButton(R.string.cancel) { dialog1: DialogInterface, _: Int -> dialog1.cancel() } + .create() + dialog.show() + } + + // Install-related methods + private fun initializeInstallSettings() { + val updateBinary = findPreference("installUpdateBinary") + updateBinary!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + InstallHelper.installCuberiteDownload(requireActivity(), StateHelper.State.NEED_DOWNLOAD_BINARY) + true + } + val updateServer = findPreference("installUpdateServer") + updateServer!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + InstallHelper.installCuberiteDownload(requireActivity(), StateHelper.State.NEED_DOWNLOAD_SERVER) + true + } + val abi = String.format(getString(R.string.settings_install_manually_abi), CuberiteHelper.preferredABI) + val setABIText = findPreference("abiText") + setABIText!!.setSummary(""" + ${setABIText.getSummary().toString()} + + $abi + """.trimIndent()) + val installBinary = findPreference("installBinary") + installBinary!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + pickFile(pickFileBinaryLauncher) + true + } + val installServer = findPreference("installServer") + installServer!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + pickFile(pickFileServerLauncher) + true + } + } + + private val installServiceCallback: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val result = intent.getStringExtra("result") + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), result!!, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + } + } + private val pickFileBinaryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> InstallHelper.installCuberiteLocal(requireActivity(), StateHelper.State.PICK_FILE_BINARY, uri) } + private val pickFileServerLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> InstallHelper.installCuberiteLocal(requireActivity(), StateHelper.State.PICK_FILE_SERVER, uri) } + private fun pickFile(launcher: ActivityResultLauncher) { + try { + launcher.launch("*/*") + } catch (e: ActivityNotFoundException) { + val message = getString(R.string.status_missing_filemanager) + Snackbar.make(requireActivity().findViewById(R.id.fragment_container), message, Snackbar.LENGTH_LONG) + .setAnchorView(requireActivity().findViewById(R.id.bottom_navigation)) + .show() + } + } + + // Info-related methods + private fun initializeInfoSettings(preferences: SharedPreferences) { + val infoDebugInfo = findPreference("infoDebugInfo") + infoDebugInfo!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val title = getString(R.string.settings_info_debug) + val message = """Running on Android ${Build.VERSION.RELEASE} (API Level ${Build.VERSION.SDK_INT}) +Using ABI ${CuberiteHelper.preferredABI} +IP: ${CuberiteHelper.getIpAddress(requireContext())} +Private directory: ${requireContext().filesDir} +Public directory: ${Environment.getExternalStorageDirectory()} +Storage location: ${preferences.getString("cuberiteLocation", "")} +Download URL: ${InstallHelper.DOWNLOAD_HOST}""" + showInfoPopup(title, message) + true + } + val thirdPartyLicenses = findPreference("thirdPartyLicenses") + thirdPartyLicenses!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val title = getString(R.string.settings_info_libraries) + val message = """ + ${getString(R.string.ini4j_license)} + + ${getString(R.string.ini4j_license_description)} + """.trimIndent() + showInfoPopup(title, message) + true + } + val version = findPreference("version") + version!!.setSummary(String.format(getString(R.string.settings_info_version), BuildConfig.VERSION_NAME)) + version.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://download.cuberite.org/android") + ) + startActivity(browserIntent) + true + } + } + + private fun showInfoPopup(title: String, message: String) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.ok) { dialog1: DialogInterface?, _: Int -> dialog1?.dismiss() } + .show() + } + + // Listeners + override fun onPause() { + super.onPause() + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(installServiceCallback) + } + + override fun onResume() { + super.onResume() + LocalBroadcastManager.getInstance(requireContext()).registerReceiver( + installServiceCallback, + IntentFilter("InstallService.callback") + ) + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/helpers/CuberiteHelper.kt b/app/src/main/kotlin/org/cuberite/android/helpers/CuberiteHelper.kt new file mode 100644 index 0000000..6d8d7fb --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/helpers/CuberiteHelper.kt @@ -0,0 +1,75 @@ +package org.cuberite.android.helpers + +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.net.wifi.WifiManager +import android.os.Build +import android.text.format.Formatter +import android.util.Log +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import org.cuberite.android.services.CuberiteService + +object CuberiteHelper { + // Logging tag + private const val LOG = "Cuberite/CuberiteHelper" + private var consoleOutput = StringBuilder() + const val EXECUTABLE_NAME = "Cuberite" + + fun addConsoleOutput(context: Context?, string: String?) { + consoleOutput.append(string).append("\n") + val intent = Intent("updateLog") + LocalBroadcastManager.getInstance(context!!).sendBroadcast(intent) + } + + fun getConsoleOutput(): String { + return consoleOutput.toString() + } + + fun resetConsoleOutput() { + consoleOutput = StringBuilder() + } + + fun getIpAddress(context: Context): String { + val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo + val ip = wifiInfo.getIpAddress() + return if (ip == 0) "127.0.0.1" else Formatter.formatIpAddress(ip) + } + + val preferredABI: String + get() { + val abi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Build.SUPPORTED_ABIS[0] + } else { + Build.CPU_ABI + } + Log.d(LOG, "Getting preferred ABI: $abi") + return abi + } + + fun isCuberiteRunning(context: Context): Boolean { + return (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) + .getRunningServices(Int.MAX_VALUE) + .any { it.service.className == CuberiteService::class.qualifiedName } + } + + fun startCuberite(context: Context) { + Log.d(LOG, "Starting Cuberite") + val serviceIntent = Intent(context, CuberiteService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent) + } else { + context.startService(serviceIntent) + } + } + + fun stopCuberite(context: Context?) { + Log.d(LOG, "Stopping Cuberite") + LocalBroadcastManager.getInstance(context!!).sendBroadcast(Intent("stop")) + } + + fun killCuberite(context: Context?) { + LocalBroadcastManager.getInstance(context!!).sendBroadcast(Intent("kill")) + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/helpers/InstallHelper.kt b/app/src/main/kotlin/org/cuberite/android/helpers/InstallHelper.kt new file mode 100644 index 0000000..8eeb079 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/helpers/InstallHelper.kt @@ -0,0 +1,38 @@ +package org.cuberite.android.helpers + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import org.cuberite.android.receivers.ProgressReceiver +import org.cuberite.android.services.InstallService + +object InstallHelper { + const val DOWNLOAD_HOST = "https://download.cuberite.org/androidbinaries/" + + fun installCuberiteDownload(activity: Activity, state: StateHelper.State?) { + val preferences = activity.getSharedPreferences(activity.packageName, Context.MODE_PRIVATE) + val intent = Intent(activity, InstallService::class.java) + .setAction("download") + .putExtra("downloadHost", DOWNLOAD_HOST) + .putExtra("state", state) + .putExtra("targetFolder", preferences.getString("cuberiteLocation", "")) + .putExtra("receiver", ProgressReceiver(activity, Handler(Looper.getMainLooper()))) + activity.startService(intent) + } + + fun installCuberiteLocal(activity: Activity, state: StateHelper.State?, selectedFileUri: Uri?) { + val preferences = activity.getSharedPreferences(activity.packageName, Context.MODE_PRIVATE) + selectedFileUri?.let { + val intent = Intent(activity, InstallService::class.java) + .setAction("unzip") + .putExtra("uri", selectedFileUri) + .putExtra("state", state) + .putExtra("targetFolder", preferences.getString("cuberiteLocation", "")) + .putExtra("receiver", ProgressReceiver(activity, Handler(Looper.getMainLooper()))) + activity.startService(intent) + } + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/helpers/StateHelper.kt b/app/src/main/kotlin/org/cuberite/android/helpers/StateHelper.kt new file mode 100644 index 0000000..43ff192 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/helpers/StateHelper.kt @@ -0,0 +1,50 @@ +package org.cuberite.android.helpers + +import android.content.Context +import android.util.Log +import java.io.File + +object StateHelper { + fun isCuberiteInstalled(context: Context): Boolean { + val state = getState(context) + return state != State.NEED_DOWNLOAD_BINARY && state != State.NEED_DOWNLOAD_SERVER && state != State.NEED_DOWNLOAD_BOTH + } + + fun getState(context: Context): State { + // Logging tag + val log = "Cuberite/State" + val preferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + var hasBinary = false + var hasServer = false + if (File(context.filesDir.absolutePath + "/" + CuberiteHelper.EXECUTABLE_NAME).exists()) { + hasBinary = true + } + if (File(preferences.getString("cuberiteLocation", "")!!).exists()) { + hasServer = true + } + + // Update state + var state = State.READY + if (CuberiteHelper.isCuberiteRunning(context)) { + state = State.RUNNING + } else if (!hasBinary && !hasServer) { + state = State.NEED_DOWNLOAD_BOTH + } else if (!hasBinary) { + state = State.NEED_DOWNLOAD_BINARY + } else if (!hasServer) { + state = State.NEED_DOWNLOAD_SERVER + } + Log.d(log, "Getting State: $state") + return state + } + + enum class State { + NEED_DOWNLOAD_SERVER, + NEED_DOWNLOAD_BINARY, + NEED_DOWNLOAD_BOTH, + PICK_FILE_BINARY, + PICK_FILE_SERVER, + RUNNING, + READY + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/preferences/MaterialListPreference.kt b/app/src/main/kotlin/org/cuberite/android/preferences/MaterialListPreference.kt new file mode 100644 index 0000000..08a28fc --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/preferences/MaterialListPreference.kt @@ -0,0 +1,28 @@ +package org.cuberite.android.preferences + +import android.content.Context +import android.content.DialogInterface +import android.util.AttributeSet +import androidx.preference.ListPreference +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class MaterialListPreference(context: Context, attrs: AttributeSet?) : ListPreference(context, attrs) { + override fun onClick() { + MaterialAlertDialogBuilder(context) + .setTitle(title) + .setCancelable(true) + .setSingleChoiceItems( + entries, + findIndexOfValue(value) + ) { dialog: DialogInterface, index: Int -> + if (callChangeListener(entryValues[index].toString())) { + setValueIndex(index) + } + dialog.dismiss() + } + .setNegativeButton( + negativeButtonText + ) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + .show() + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/receivers/ProgressReceiver.kt b/app/src/main/kotlin/org/cuberite/android/receivers/ProgressReceiver.kt new file mode 100644 index 0000000..2d709b8 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/receivers/ProgressReceiver.kt @@ -0,0 +1,61 @@ +package org.cuberite.android.receivers + +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.ResultReceiver +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.progressindicator.LinearProgressIndicator +import org.cuberite.android.R + +class ProgressReceiver(private val cont: Context, handler: Handler?) : ResultReceiver(handler) { + private var progressDialog: AlertDialog? = null + private lateinit var progressBar: LinearProgressIndicator + + private fun createDialog(title: String?) { + val layout = View.inflate(cont, R.layout.dialog_progress, null) + progressBar = layout.findViewById(R.id.progressBar) as LinearProgressIndicator + progressDialog = MaterialAlertDialogBuilder(cont) + .setTitle(title) + .setView(layout) + .setCancelable(false) + .create() + } + + public override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { + super.onReceiveResult(resultCode, resultData) + when (resultCode) { + PROGRESS_START -> { + val title = resultData!!.getString("title") + progressDialog?.let { + progressDialog!!.setTitle(title) + } ?: run { + createDialog(title) + } + progressBar.isIndeterminate = true + progressDialog!!.show() + } + + PROGRESS_NEW_DATA -> { + val progress = resultData!!.getInt("progress") + val max = resultData.getInt("max") + progressBar.isIndeterminate = false + progressBar.setProgressCompat(progress, true) + progressBar.setMax(max) + } + + PROGRESS_END -> { + progressDialog!!.dismiss() + progressDialog = null + } + } + } + + companion object { + const val PROGRESS_START = 0 + const val PROGRESS_NEW_DATA = 1 + const val PROGRESS_END = 2 + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/receivers/StartupReceiver.kt b/app/src/main/kotlin/org/cuberite/android/receivers/StartupReceiver.kt new file mode 100644 index 0000000..7d787fa --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/receivers/StartupReceiver.kt @@ -0,0 +1,19 @@ +package org.cuberite.android.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import org.cuberite.android.helpers.CuberiteHelper +import org.cuberite.android.helpers.StateHelper + +class StartupReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (Intent.ACTION_BOOT_COMPLETED == intent.action) { + val preferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + if (preferences.getBoolean("startOnBoot", false) + && StateHelper.isCuberiteInstalled(context)) { + CuberiteHelper.startCuberite(context) + } + } + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/services/CuberiteService.kt b/app/src/main/kotlin/org/cuberite/android/services/CuberiteService.kt new file mode 100644 index 0000000..f0fadcf --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/services/CuberiteService.kt @@ -0,0 +1,178 @@ +package org.cuberite.android.services + +import android.app.IntentService +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.NetworkInfo +import android.net.wifi.WifiManager +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import org.cuberite.android.MainActivity +import org.cuberite.android.R +import org.cuberite.android.helpers.CuberiteHelper +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.util.Scanner + +class CuberiteService : IntentService("CuberiteService") { + // Logging tag + private val log = "Cuberite/ServerService" + private lateinit var notification: NotificationCompat.Builder + private lateinit var process: Process + private lateinit var cuberiteSTDIN: OutputStream + + // Notification-related methods + private fun createNotification() { + val channelId = "cuberiteservice" + val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher + val text = getText(R.string.notification_cuberite_running) + val ip = CuberiteHelper.getIpAddress(applicationContext) + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 + val notificationIntent = Intent(this, MainActivity::class.java) + val contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags) + notification = NotificationCompat.Builder(this, channelId) + .setSmallIcon(icon) + .setTicker(text) + .setContentTitle(text) + .setContentText(ip) + .setContentIntent(contentIntent) + .setOnlyAlertOnce(true) + .setOngoing(true) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + startForeground(1, notification.build()) + } + + // Process-related methods + @Throws(IOException::class) + private fun startProcess() { + val preferences = applicationContext.getSharedPreferences(this.packageName, MODE_PRIVATE) + val executableName = CuberiteHelper.EXECUTABLE_NAME + val location = preferences.getString("cuberiteLocation", "") + + // Clear previous output + CuberiteHelper.resetConsoleOutput() + + // Make sure we can execute the binary + File(this.filesDir, executableName).setExecutable(true, true) + + // Initiate ProcessBuilder with the command at the given location + val processBuilder = ProcessBuilder(this.filesDir.toString() + "/" + executableName, "--no-output-buffering") + processBuilder.directory(File(location!!)) + processBuilder.redirectErrorStream(true) + CuberiteHelper.addConsoleOutput(applicationContext, "Info: Cuberite is starting...") + Log.d(log, "Starting process...") + process = processBuilder.start() + cuberiteSTDIN = process.outputStream + } + + private fun updateOutput() { + Log.d(log, "Starting logging...") + val processScanner = Scanner(process.inputStream) + var line: String? + try { + while (processScanner.nextLine().also { line = it } != null) { + Log.i(log, line!!) + CuberiteHelper.addConsoleOutput(applicationContext, line) + } + } catch (e: NoSuchElementException) { + // Do nothing. Workaround for issues in older Android versions. + } + processScanner.close() + } + + // Broadcast receivers + private val executeCommand: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val command = intent.getStringExtra("message") + try { + cuberiteSTDIN.write((command + "\n").toByteArray()) + cuberiteSTDIN.flush() + } catch (e: Exception) { + Log.e(log, "An error occurred when writing $command to the STDIN", e) + } + } + } + private val updateIp: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (WifiManager.NETWORK_STATE_CHANGED_ACTION == action) { + val info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO) + if (NetworkInfo.State.CONNECTED == info!!.state || NetworkInfo.State.DISCONNECTED == info.state) { + Log.d(log, "Updating notification IP due to network change") + val ip = CuberiteHelper.getIpAddress(context) + notification.setContentText(ip) + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(1, notification.build()) + } + } + } + } + private val stop: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + try { + cuberiteSTDIN.write("stop\n".toByteArray()) + cuberiteSTDIN.flush() + } catch (e: Exception) { + Log.e(log, "An error occurred when writing stop to the STDIN", e) + } + } + } + private val kill: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + process.destroy() + } + } + + // Service startup and cleanup + @Deprecated("Deprecated in Java") + override fun onHandleIntent(intent: Intent?) { + Log.d(log, "Starting service...") + try { + // Create and show notification about Cuberite running + createNotification() + + // Start the Cuberite process + startProcess() + + // Update notification IP if network changes + val intentFilter = IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION) + registerReceiver(updateIp, intentFilter) + + // Communication with the activity + LocalBroadcastManager.getInstance(this).registerReceiver(executeCommand, IntentFilter("executeCommand")) + LocalBroadcastManager.getInstance(this).registerReceiver(stop, IntentFilter("stop")) + LocalBroadcastManager.getInstance(this).registerReceiver(kill, IntentFilter("kill")) + + // Log to console + val logTimeStart = System.currentTimeMillis() + updateOutput() + + // Logic waits here until Cuberite has stopped. Everything after that is cleanup for the next run + val logTimeEnd = System.currentTimeMillis() + if (logTimeEnd - logTimeStart < 100) { + LocalBroadcastManager.getInstance(this).sendBroadcast(Intent("showStartupError")) + } + + // Shutdown + unregisterReceiver(updateIp) + LocalBroadcastManager.getInstance(this).unregisterReceiver(executeCommand) + LocalBroadcastManager.getInstance(this).unregisterReceiver(stop) + LocalBroadcastManager.getInstance(this).unregisterReceiver(kill) + cuberiteSTDIN.close() + } catch (e: Exception) { + Log.e(log, "An error occurred when starting Cuberite", e) + + // Send error to user + LocalBroadcastManager.getInstance(this).sendBroadcast(Intent("showStartupError")) + } + stopSelf() + LocalBroadcastManager.getInstance(this).sendBroadcast(Intent("CuberiteService.callback")) + } +} diff --git a/app/src/main/kotlin/org/cuberite/android/services/InstallService.kt b/app/src/main/kotlin/org/cuberite/android/services/InstallService.kt new file mode 100644 index 0000000..fbffbf9 --- /dev/null +++ b/app/src/main/kotlin/org/cuberite/android/services/InstallService.kt @@ -0,0 +1,275 @@ +package org.cuberite.android.services + +import android.app.IntentService +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.PowerManager +import android.os.PowerManager.WakeLock +import android.os.ResultReceiver +import android.util.Log +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import org.cuberite.android.R +import org.cuberite.android.helpers.CuberiteHelper +import org.cuberite.android.helpers.StateHelper +import org.cuberite.android.receivers.ProgressReceiver +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest +import java.util.Scanner +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +class InstallService : IntentService("InstallService") { + // Logging tag + private val log = "Cuberite/InstallService" + private var receiver: ResultReceiver? = null + + // Wakelock + private fun acquireWakelock(): WakeLock { + Log.d(log, "Acquiring wakeLock") + val pm = getSystemService(POWER_SERVICE) as PowerManager + val wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, javaClass.getName()) + wakeLock.acquire(300000) // 5 min timeout + return wakeLock + } + + // Download verification + private fun downloadVerify(url: String, targetLocation: File, retryCount: Int): String? { + val zipFileError = download(url, targetLocation) + zipFileError?.let { + return zipFileError + } + + // Verifying file + val shaError = download("$url.sha1", File("$targetLocation.sha1")) + shaError?.let { + return shaError + } + try { + val generatedSha = generateSha1(targetLocation) + val downloadedSha = Scanner( + File("$targetLocation.sha1") + ) + .useDelimiter("\\Z") + .next() + .split(" ".toRegex(), limit = 2).toTypedArray()[0] + File("$targetLocation.sha1").delete() + if (downloadedSha != generatedSha) { + Log.d(log, "SHA-1 check didn't pass") + return if (retryCount > 0) { + // Retry if verification failed + downloadVerify(url, targetLocation, retryCount - 1) + } else getString(R.string.status_shasum_error) + } + Log.d(log, "SHA-1 check passed successfully with checksum $generatedSha") + } catch (e: FileNotFoundException) { + Log.e(log, "Something went wrong while generating checksum", e) + return getString(R.string.status_shasum_error) + } + return null + } + + private fun generateSha1(targetLocation: File): String { + return try { + val sha1 = MessageDigest.getInstance("SHA-1") + val input: InputStream = FileInputStream(targetLocation) + val buffer = ByteArray(8192) + var len = input.read(buffer) + while (len != -1) { + sha1.update(buffer, 0, len) + len = input.read(buffer) + } + val shaSum = sha1.digest() + val charset = "0123456789ABCDEF".toCharArray() + val hexResult = CharArray(shaSum.size * 2) + for (j in shaSum.indices) { + val v = shaSum[j].toInt() and 0xFF + hexResult[j * 2] = charset[v ushr 4] + hexResult[j * 2 + 1] = charset[v and 0x0F] + } + String(hexResult).lowercase() + } catch (e: Exception) { + e.toString() + } + } + + // Download + private fun download(stringUrl: String, targetLocation: File): String? { + val wakeLock = acquireWakelock() + val bundleInit = Bundle() + val url = URL(stringUrl) + val connection = url.openConnection() as HttpURLConnection + var inputStream: InputStream? = null + var outputStream: OutputStream? = null + var result: String? = null + + bundleInit.putString("title", getString(R.string.status_downloading_cuberite)) + receiver!!.send(ProgressReceiver.PROGRESS_START, bundleInit) + Log.d(log, "Started downloading $stringUrl") + Log.d(log, "Downloading to $targetLocation") + + try { + connection.setConnectTimeout(10000) // 10 secs + connection.connect() + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + val error = "Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage() + Log.e(log, error) + result = error + } else { + inputStream = connection.inputStream + outputStream = FileOutputStream(targetLocation) + val length = connection.getContentLength() + val data = ByteArray(4096) + var total: Long = 0 + var count: Int + while (inputStream.read(data).also { count = it } != -1) { + total += count.toLong() + if (length > 0) { // only if total length is known + val bundleProg = Bundle() + bundleProg.putInt("progress", total.toInt()) + bundleProg.putInt("max", length) + receiver!!.send(ProgressReceiver.PROGRESS_NEW_DATA, bundleProg) + } + outputStream.write(data, 0, count) + } + Log.d(log, "Finished downloading") + } + } catch (e: Exception) { + result = e.message + Log.e(log, "An error occurred when downloading a zip", e) + } finally { + try { + inputStream?.let { + inputStream.close() + } + outputStream?.let { + outputStream.close() + } + } catch (ignored: IOException) { + } + connection.disconnect() + } + result?.let { + receiver!!.send(ProgressReceiver.PROGRESS_END, null) + } + Log.d(log, "Releasing wakeLock") + wakeLock.release() + return result + } + + // Unzip + private fun unzip(fileUri: Uri?, targetLocation: File): String { + var result = getString(R.string.status_install_success) + Log.i(log, "Unzipping $fileUri to $targetLocation") + val wakeLock = acquireWakelock() + if (!targetLocation.exists()) { + targetLocation.mkdir() + } + + // Create a .nomedia file in the server directory to prevent images from showing in gallery + createNoMediaFile(targetLocation) + val bundleInit = Bundle() + bundleInit.putString("title", getString(R.string.status_installing_cuberite)) + receiver!!.send(ProgressReceiver.PROGRESS_START, bundleInit) + try { + unzipStream(fileUri, targetLocation) + } catch (e: IOException) { + result = getString(R.string.status_unzip_error) + Log.e(log, "An error occurred while installing Cuberite", e) + } + receiver!!.send(ProgressReceiver.PROGRESS_END, null) + Log.d(log, "Releasing wakeLock") + wakeLock.release() + return result + } + + @Throws(IOException::class) + private fun unzipStream(fileUri: Uri?, targetLocation: File) { + val inputStream = contentResolver.openInputStream(fileUri!!) + val zipInputStream = ZipInputStream(inputStream) + var zipEntry: ZipEntry + while (zipInputStream.getNextEntry().also { zipEntry = it } != null) { + if (zipEntry.isDirectory) { + File(targetLocation, zipEntry.name).mkdir() + } else { + val outputStream = FileOutputStream(targetLocation.toString() + "/" + zipEntry.name) + val bufferedOutputStream = BufferedOutputStream(outputStream) + val buffer = ByteArray(1024) + var read: Int + while (zipInputStream.read(buffer).also { read = it } != -1) { + bufferedOutputStream.write(buffer, 0, read) + } + zipInputStream.closeEntry() + bufferedOutputStream.close() + outputStream.close() + } + } + zipInputStream.close() + } + + private fun createNoMediaFile(targetFolder: File) { + val noMedia = File(targetFolder, ".nomedia") + try { + noMedia.createNewFile() + } catch (e: IOException) { + Log.e(log, "Something went wrong while creating the .nomedia file", e) + } + } + + // Service handler + @Deprecated("Deprecated in Java") + override fun onHandleIntent(intent: Intent?) { + val state = intent!!.getSerializableExtra("state") as StateHelper.State? + var result: String? + if ((state == StateHelper.State.NEED_DOWNLOAD_BINARY || state == StateHelper.State.NEED_DOWNLOAD_BOTH || state == StateHelper.State.PICK_FILE_BINARY) + && CuberiteHelper.isCuberiteRunning(applicationContext)) { + result = getString(R.string.status_update_binary_error) + } else if ("unzip" == intent.action) { + val uri = intent.getParcelableExtra("uri") + val targetFolder = File( + if (state == StateHelper.State.PICK_FILE_BINARY) this.filesDir.absolutePath else intent.getStringExtra("targetFolder")!! + ) + receiver = intent.getParcelableExtra("receiver") + result = unzip(uri, targetFolder) + } else { + val downloadHost = intent.getStringExtra("downloadHost") + val abi = CuberiteHelper.preferredABI + val targetFileName = (if (state == StateHelper.State.NEED_DOWNLOAD_BINARY || state == StateHelper.State.NEED_DOWNLOAD_BOTH) abi else "server") + ".zip" + val downloadUrl = downloadHost + targetFileName + val tempZip = File(this.cacheDir, targetFileName) // Zip files are temporary + val targetFolder = File( + if (state == StateHelper.State.NEED_DOWNLOAD_BINARY || state == StateHelper.State.NEED_DOWNLOAD_BOTH) this.filesDir.absolutePath else intent.getStringExtra("targetFolder")!! + ) + receiver = intent.getParcelableExtra("receiver") + + // Download + Log.i(log, "Downloading $state") + val retryCount = 1 + result = downloadVerify(downloadUrl, tempZip, retryCount) + if (result == null) { + result = unzip(Uri.fromFile(tempZip), targetFolder) + if (!tempZip.delete()) { + Log.w(log, "Failed to delete downloaded zip file") + } + } + if (state == StateHelper.State.NEED_DOWNLOAD_BOTH) { + intent.putExtra("state", StateHelper.State.NEED_DOWNLOAD_SERVER) + onHandleIntent(intent) + } + } + stopSelf() + LocalBroadcastManager.getInstance(this).sendBroadcast( + Intent("InstallService.callback") + .putExtra("result", result) + ) + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index c015c21..0000000 --- a/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - mavenCentral() - maven { - url 'https://maven.google.com/' - name 'Google' - } - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.3.1' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - mavenCentral() - maven { - url 'https://maven.google.com/' - name 'Google' - } - } -} - -tasks.register('clean', Delete) { - delete rootProject.layout.buildDirectory -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b271bd6 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,26 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath("com.android.tools.build:gradle:8.3.1") + classpath(kotlin("gradle-plugin", version = "2.0.0-Beta5")) + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle.kts files + } +} + +allprojects { + repositories { + mavenCentral() + google() + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.layout.buildDirectory) +} diff --git a/gradle.properties b/gradle.properties index 878b145..252ab53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,11 +9,11 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -android.injected.testOnly=false -android.nonFinalResIds=false -android.nonTransitiveRClass=false +android.enableBuildConfigAsBytecode=true android.useAndroidX=true +org.gradle.configuration-cache=true org.gradle.jvmargs=-Xmx1536m +org.gradle.warning.mode=all # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3bafa98..eada7a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,8 @@ +#Fri Mar 22 21:25:04 EET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip -distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae +distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists