/* * This file is part of AndroidIDE. * * AndroidIDE is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * AndroidIDE is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . * */ package com.itsaky.androidide; import android.content.Intent; import android.content.res.ColorStateList; import android.os.Bundle; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import androidx.core.content.ContextCompat; import androidx.transition.TransitionManager; import com.blankj.utilcode.util.FileIOUtils; import com.blankj.utilcode.util.SizeUtils; import com.blankj.utilcode.util.ThreadUtils; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.itsaky.androidide.app.StudioActivity; import com.itsaky.androidide.databinding.ActivityDownloadBinding; import com.itsaky.androidide.fragments.sheets.ProgressSheet; import com.itsaky.androidide.managers.PreferenceManager; import com.itsaky.androidide.shell.IProcessExecutor; import com.itsaky.androidide.shell.ProcessExecutorFactory; import com.itsaky.androidide.shell.ProcessStreamsHolder; import com.itsaky.androidide.tasks.TaskExecutor; import com.itsaky.androidide.tasks.callables.ListDirectoryCallable; import com.itsaky.androidide.utils.DialogUtils; import com.itsaky.androidide.utils.Environment; import com.itsaky.androidide.utils.FileUtil; import com.itsaky.androidide.utils.InputStreamLineReader; import com.itsaky.toaster.Toaster; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import abhishekti7.unicorn.filepicker.UnicornFilePicker; public class DownloadActivity extends StudioActivity { private ActivityDownloadBinding binding; private ProgressSheet progressSheet; private File choosenDir = null; @Override protected View bindLayout() { binding = ActivityDownloadBinding.inflate(getLayoutInflater()); return binding.getRoot(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final OnClickListener clickListener = this::handleClick; binding.dismiss.setOnClickListener(clickListener); binding.choose.setOnClickListener(clickListener); binding.install.setOnClickListener(clickListener); binding.install.setEnabled(false); } private void handleClick(View v) { final int id = v.getId(); if (id == binding.dismiss.getId()) { TransitionManager.beginDelayedTransition(binding.getRoot()); binding.card.setVisibility(View.GONE); } else if (id == binding.choose.getId()) { try { binding.install.setEnabled(false); UnicornFilePicker.from(this) .addConfigBuilder() .addItemDivider(false) .selectMultipleFiles(false) .setRootDirectory(FileUtil.getExternalStorageDir()) .showHiddenFiles(true) .showOnlyDirectory(true) .theme(R.style.AppTheme_FilePicker) .build() .forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE); } catch (Throwable th) { LOG.error(getString(R.string.err_cannot_start_file_picker), th); } } else if (id == binding.install.getId()) { if (choosenDir == null) { getApp().toast(R.string.msg_pick_files, Toaster.Type.ERROR); return; } if (!choosenDir.exists()) { getApp().toast(R.string.msg_file_doesnt_exist, Toaster.Type.ERROR); return; } installAll(); } } private void installAll() { showProgress(); getApp().getPrefManager().putBoolean(PreferenceManager.KEY_FRAMEWORK_DOWNLOADED, true); try { final File script = createExtractScript(); final ProcessStreamsHolder holder = new ProcessStreamsHolder(); final IProcessExecutor executor = ProcessExecutorFactory.commonExecutor(); executor.execAsync( holder, this::onInstallProcessExit, true, Environment.BUSYBOX.getAbsolutePath(), "sh", // We use busybox's sh, because Environment.SHELL is not installed yet... script.getAbsolutePath()); this.output = new StringBuilder(); final InputStreamLineReader reader = new InputStreamLineReader(holder.in, this::onInstallationOutput); new Thread(reader).start(); } catch (DownloadActivity.InstallationException e) { LOG.error(getString(R.string.err_installation), e); onInstallationFailed(e.exitCode); } catch (IOException e) { LOG.error(getString(R.string.err_installation), e); onInstallationFailed(5); // Exit code 5 : I/O Error } } private void onInstallationOutput(final String line) { ThreadUtils.runOnUiThread(() -> this.appendOut(line)); } private void onInstallProcessExit(final int code) { ThreadUtils.runOnUiThread( () -> { if (code == 0) { // 0 = normal execution getApp().getPrefManager().putBoolean(PreferenceManager.KEY_FRAMEWORK_INSTALLED, true); showRestartNeeded(); if (getProgressSheet().isShowing()) { getProgressSheet().dismiss(); } } else { onInstallationFailed(code); } }); } private void onInstallationFailed(int code) { // TODO Can this be improved by adding some animations? if (getProgressSheet().isShowing()) { getProgressSheet().dismiss(); } final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this); builder.setTitle(R.string.title_installation_failed); builder.setMessage(getString(R.string.msg_installation_failed, code, this.output.toString())); builder.setPositiveButton( android.R.string.ok, (d, w) -> { d.dismiss(); finishAffinity(); }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); } private File createExtractScript() throws DownloadActivity.InstallationException { final StringBuilder sb = new StringBuilder(); sb.append("cd"); joiner(sb); sb.append("echo 'Installing...'"); joiner(sb); File[] files = choosenDir.listFiles(ARCHIVE_FILTER); if (files == null || files.length <= 0) { throw new InstallationException(2); } for (File f : files) { if (f.getName().endsWith(".tar.xz")) { if (f.getName().startsWith("androidide-sysroot")) { sb.append("cd $SYSROOT/.."); joiner(sb); } sb.append("$BUSYBOX tar xvJf '").append(f.getAbsolutePath()).append("'"); joiner(sb); sb.append("cd $HOME"); joiner(sb); } else if (f.getName().endsWith(".zip")) { sb.append("$BUSYBOX unzip '").append(f.getAbsolutePath()).append("'"); joiner(sb); } } sb.append("echo 'Cleaning unsupported flags in binaries...'"); joiner(sb); String DONE = "DONE"; sb.append("echo ").append(DONE); final File script = new File(Environment.TMP_DIR, "extract_tools.sh"); if (!FileIOUtils.writeFileFromString(script, sb.toString())) { throw new InstallationException(2); } return script; } private void joiner(StringBuilder sb) { sb.append(" && "); } private StringBuilder output = new StringBuilder(); private void appendOut(String line) { LOG.debug("Installation:", line); output.append(line.trim()); output.append("\n"); getProgressSheet().setSubMessage(line); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE && resultCode == RESULT_OK) { ArrayList files = data.getStringArrayListExtra("filePaths"); if (files != null) { if (files.size() == 1) { File choosenDir = new File(files.get(0)); if (choosenDir.exists() && choosenDir.isDirectory()) { this.choosenDir = choosenDir; setDone(binding.choose); binding.install.setEnabled(true); new TaskExecutor() .executeAsync( new ListDirectoryCallable(choosenDir), __ -> binding.filesText.setText(getString(R.string.msg_installable_files, __))); } else { getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR); } } else { getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR); } } } } private void setDone(MaterialButton button) { final int dp8 = SizeUtils.dp2px(8); final int dp4 = SizeUtils.dp2px(4); final int dp24 = SizeUtils.dp2px(24); button.setText(""); button.setIconResource(R.drawable.ic_ok); button.setIconTint(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.bg_green))); button.setBackgroundColor(android.graphics.Color.TRANSPARENT); button.setPaddingRelative(dp8, dp4, dp8, dp4); button.setStrokeWidth(0); button.setGravity(Gravity.CENTER); button.setIconPadding(0); button.setInsetTop(0); button.setInsetBottom(0); button.setPaddingRelative(dp24, dp4, dp24, dp4); } private void showProgress() { getProgressSheet().setCancelable(false); getProgressSheet().setShowShadow(false); getProgressSheet() .setSubMessageEnabled(true) .setWelcomeTextEnabled(true) .show(getSupportFragmentManager(), "progress_sheet"); } private void showRestartNeeded() { setDone(binding.install); final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this); builder.setTitle(R.string.title_restart); builder.setMessage(R.string.msg_restart); builder.setCancelable(false); builder.setPositiveButton(android.R.string.ok, (p1, p2) -> finishAffinity()); builder.create().show(); } private ProgressSheet getProgressSheet() { return progressSheet == null ? progressSheet = new ProgressSheet().setMessage(getString(R.string.please_wait)) : progressSheet; } private final FileFilter ARCHIVE_FILTER = p1 -> p1.isFile() && (p1.getName().endsWith(".tar.xz") || p1.getName().endsWith(".zip")); private static class InstallationException extends Exception { private final int exitCode; public InstallationException(int exitCode) { this.exitCode = exitCode; } } }