feat: use Flashbar for project sync notification

This commit is contained in:
Akash Yadav
2023-03-28 19:17:10 +05:30
parent a7e9f350ae
commit e6d1e42004
8 changed files with 72 additions and 269 deletions
@@ -21,9 +21,11 @@ import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
/** @author Akash Yadav */
@@ -52,34 +54,40 @@ class SaveFileAction(context: Context) : EditorRelatedAction() {
enabled = context.areFilesModified()
}
override fun execAction(data: ActionData): Boolean {
val context = data.getActivity() ?: return false
override fun execAction(data: ActionData): ResultWrapper {
val context = data.getActivity() ?: return ResultWrapper()
return try {
// Cannot use context.saveAll() because this.execAction is called on non-UI thread
// and saveAll call will result in UI actions
val result = context.saveAllResult()
if (result.xmlSaved) {
ProjectManager.generateSources()
}
if (result.gradleSaved) {
context.notifySyncNeeded()
}
true
ResultWrapper(context.saveAllResult())
} catch (error: Throwable) {
log.error("Failed to save file", error)
false
ResultWrapper()
}
}
override fun postExec(data: ActionData, result: Any) {
if (result is Boolean && result) {
if (result is ResultWrapper && result.result != null) {
val context = data.requireActivity()
// show save notification before calling 'notifySyncNeeded' so that the file save notification
// does not overlap the sync notification
flashSuccess(R.string.all_saved)
val saveResult = result.result
if (saveResult.xmlSaved) {
ProjectManager.generateSources()
}
if (saveResult.gradleSaved) {
context.notifySyncNeeded()
}
} else {
log.error("Failed to save file")
TODO("Create message in strings.xml")
flashError(R.string.save_failed)
}
}
inner class ResultWrapper(val result: SaveResult? = null)
}
@@ -80,16 +80,19 @@ import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath
import com.itsaky.androidide.projects.ProjectManager.projectPath
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.services.LogReceiver
import com.itsaky.androidide.ui.MaterialBanner
import com.itsaky.androidide.ui.editor.CodeEditorView
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.utils.ActionMenuUtils.createMenu
import com.itsaky.androidide.utils.ApkInstallationSessionCallback
import com.itsaky.androidide.utils.DURATION_INDEFINITE
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashbarBuilder
import com.itsaky.androidide.utils.resolveAttr
import com.itsaky.androidide.utils.showOnUiThread
import com.itsaky.androidide.utils.withIcon
import com.itsaky.androidide.viewmodel.EditorViewModel
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
@@ -191,13 +194,11 @@ abstract class BaseEditorActivity :
val packageName = onResult(this, intent)
if (packageName != null) {
Snackbar.make(binding.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG)
.setAction(string.yes) {
tryLaunchApp(packageName)
}
.setAction(string.yes) { tryLaunchApp(packageName) }
.show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -385,13 +386,23 @@ abstract class BaseEditorActivity :
fun notifySyncNeeded(onConfirm: () -> Unit) {
val buildService = Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE)
if (buildService != null && !buildService.isBuildInProgress) {
getSyncBanner()?.apply {
setNegative(android.R.string.cancel, null)
setPositive(android.R.string.ok) { onConfirm() }
show()
if (buildService == null || buildService.isBuildInProgress) return
flashbarBuilder(
duration = DURATION_INDEFINITE,
backgroundColor = resolveAttr(attr.colorSecondaryContainer),
messageColor = resolveAttr(attr.colorOnSecondaryContainer)
)
.withIcon(drawable.ic_sync, colorFilter = resolveAttr(attr.colorOnSecondaryContainer))
.message(string.msg_sync_needed)
.positiveActionText(string.btn_sync)
.positiveActionTapListener {
onConfirm()
it.dismiss()
}
}
.negativeActionText(string.btn_ignore_changes)
.negativeActionTapListener { it.dismiss() }
.showOnUiThread()
}
open fun getFileTreeFragment(): FileTreeFragment? {
@@ -406,7 +417,7 @@ abstract class BaseEditorActivity :
viewModel.statusText = text
viewModel.statusGravity = gravity
}
private fun tryLaunchApp(packageName: String) {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
launchIntent?.let {
@@ -629,17 +640,6 @@ abstract class BaseEditorActivity :
builder.create().show()
}
private fun getSyncBanner(): MaterialBanner? {
return binding.run {
return@run syncBanner
.setContentTextColor(resolveAttr(attr.colorOnPrimaryContainer))
.setBannerBackgroundColor(resolveAttr(attr.colorPrimaryContainer))
.setButtonTextColor(resolveAttr(attr.colorOnPrimaryContainer))
.setIcon(drawable.ic_sync)
.setContentText(string.msg_sync_needed)
}
}
open fun installationSessionCallback(): SessionCallback {
return ApkInstallationSessionCallback(this).also { installationCallback = it }
}
@@ -1,209 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.ui;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
import com.itsaky.androidide.databinding.LayoutBannerBinding;
public class MaterialBanner extends CoordinatorLayout {
private LayoutBannerBinding binding;
private OnPositiveClickListener positiveListener;
private OnNegativeClickListener negativeListener;
private String contentText;
private String leftButtonText;
private String rightButtonText;
private Drawable iconDrawable;
private int backgroundColor;
private int contentTextColor;
private int buttonTextColor;
public MaterialBanner(Context context) {
this(context, null);
}
public MaterialBanner(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MaterialBanner(Context context, AttributeSet attrs, int style) {
super(context, attrs, style);
init();
}
private void init() {
binding = LayoutBannerBinding.inflate(LayoutInflater.from(getContext()), this, true);
}
public void show() {
this.binding.contentTextView.setText(contentText);
this.binding.contentIconView.setImageDrawable(iconDrawable);
this.binding.rightButton.setText(rightButtonText);
this.binding.leftButton.setText(leftButtonText);
this.binding.getRoot().setBackgroundColor(backgroundColor);
this.binding.contentTextView.setTextColor(contentTextColor);
this.binding.leftButton.setTextColor(buttonTextColor);
this.binding.rightButton.setTextColor(buttonTextColor);
this.binding.rightButton.setOnClickListener(
v -> {
if (positiveListener == null) dismiss();
else {
positiveListener.onClick(MaterialBanner.this);
dismiss();
}
});
this.binding.leftButton.setOnClickListener(
v -> {
if (negativeListener == null) dismiss();
else {
negativeListener.onClick(MaterialBanner.this);
dismiss();
}
});
final ConstraintLayout root = this.binding.getRoot();
root.measure(-1, -2);
final int height = root.getMeasuredHeight();
final LayoutParams p = (LayoutParams) root.getLayoutParams();
p.height = 0;
root.setLayoutParams(p);
root.setVisibility(VISIBLE);
setVisibility(VISIBLE);
Animation anim =
new Animation() {
@Override
public boolean willChangeBounds() {
return true;
}
@Override
public void applyTransformation(float time, Transformation t) {
p.height = time == 1f ? -2 : (int) (height * time);
root.setLayoutParams(p);
root.requestLayout();
}
};
anim.setDuration(200);
root.startAnimation(anim);
}
public void dismiss() {
final ConstraintLayout root = this.binding.getRoot();
final LayoutParams p = (LayoutParams) root.getLayoutParams();
final int initialHeight = getMeasuredHeight();
Animation animation =
new Animation() {
@Override
public boolean willChangeBounds() {
return true;
}
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1f) {
root.setVisibility(GONE);
setVisibility(GONE);
} else {
p.height = (int) (initialHeight - (initialHeight * interpolatedTime));
root.requestLayout();
}
}
};
animation.setDuration((long) (initialHeight / getResources().getDisplayMetrics().density));
root.startAnimation(animation);
}
public MaterialBanner setIcon(@DrawableRes int icon) {
return setIcon(ContextCompat.getDrawable(getContext(), icon));
}
public MaterialBanner setIcon(Drawable icon) {
this.iconDrawable = icon;
return this;
}
public MaterialBanner setPositive(@StringRes int text, OnPositiveClickListener listener) {
return setPositive(getContext().getString(text), listener);
}
public MaterialBanner setPositive(String text, OnPositiveClickListener listener) {
this.rightButtonText = text;
this.positiveListener = listener;
return this;
}
public MaterialBanner setNegative(@StringRes int text, OnNegativeClickListener listener) {
return setNegative(getContext().getString(text), listener);
}
public MaterialBanner setNegative(String text, OnNegativeClickListener listener) {
this.leftButtonText = text;
this.negativeListener = listener;
return this;
}
public MaterialBanner setContentText(@StringRes int text) {
return setContentText(getContext().getString(text));
}
public MaterialBanner setContentText(String contentText) {
this.contentText = contentText;
return this;
}
public MaterialBanner setBannerBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
return this;
}
public MaterialBanner setContentTextColor(int contentTextColor) {
this.contentTextColor = contentTextColor;
return this;
}
public MaterialBanner setButtonTextColor(int buttonTextColor) {
this.buttonTextColor = buttonTextColor;
return this;
}
public static interface OnPositiveClickListener {
public void onClick(MaterialBanner banner);
}
public static interface OnNegativeClickListener {
public void onClick(MaterialBanner banner);
}
}
@@ -32,12 +32,6 @@
android:layout_height="wrap_content"
android:indeterminate="true" />
<com.itsaky.androidide.ui.MaterialBanner
android:id="@+id/syncBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
style="@style/AppTheme.TabLayout"
+1 -1
View File
@@ -25,10 +25,10 @@ dependencies {
implementation(projects.eventbusEvents)
implementation(projects.lexers)
implementation(projects.resources)
implementation(projects.subprojects.flashbar)
api(projects.shared)
api(projects.logger)
api(projects.subprojects.flashbar)
testImplementation(libs.tests.junit)
testImplementation(libs.tests.google.truth)
@@ -36,19 +36,25 @@ import com.itsaky.androidide.utils.FlashType.ERROR
import com.itsaky.androidide.utils.FlashType.INFO
import com.itsaky.androidide.utils.FlashType.SUCCESS
const val LENGTH_SHORT = 2000L
const val LENGTH_LONG = 3500L
const val DURATION_SHORT = 2000L
const val DURATION_LONG = 3500L
const val DURATION_INDEFINITE = Flashbar.DURATION_INDEFINITE
private val COLOR_SUCCESS = Color.parseColor("#4CAF50")
private val COLOR_ERROR = Color.parseColor("#f44336")
private const val COLOR_INFO = Color.DKGRAY
val COLOR_SUCCESS = Color.parseColor("#4CAF50")
val COLOR_ERROR = Color.parseColor("#f44336")
const val COLOR_INFO = Color.DKGRAY
fun Activity.flashbarBuilder(duration: Long = LENGTH_SHORT): Flashbar.Builder {
fun Activity.flashbarBuilder(
gravity: Flashbar.Gravity = TOP,
duration: Long = DURATION_SHORT,
backgroundColor: Int = resolveAttr(R.attr.colorPrimaryContainer),
messageColor: Int = resolveAttr(R.attr.colorOnPrimaryContainer)
): Flashbar.Builder {
return Flashbar.Builder(this)
.gravity(TOP)
.gravity(gravity)
.duration(duration)
.backgroundColor(resolveAttr(R.attr.colorPrimaryContainer))
.messageColor(resolveAttr(R.attr.colorOnPrimaryContainer))
.backgroundColor(backgroundColor)
.messageColor(messageColor)
}
fun Activity.flashMessage(msg: String?, type: FlashType) {
@@ -107,19 +113,19 @@ fun Flashbar.showOnUiThread() {
}
}
private fun Flashbar.Builder.successIcon(): Flashbar.Builder {
fun Flashbar.Builder.successIcon(): Flashbar.Builder {
return withIcon(R.drawable.ic_ok, colorFilter = COLOR_SUCCESS)
}
private fun Flashbar.Builder.errorIcon(): Flashbar.Builder {
fun Flashbar.Builder.errorIcon(): Flashbar.Builder {
return withIcon(R.drawable.ic_error, colorFilter = COLOR_ERROR)
}
private fun Flashbar.Builder.infoIcon(): Flashbar.Builder {
fun Flashbar.Builder.infoIcon(): Flashbar.Builder {
return withIcon(R.drawable.ic_info, colorFilter = COLOR_INFO)
}
private fun Flashbar.Builder.withIcon(
fun Flashbar.Builder.withIcon(
@DrawableRes icon: Int,
@FloatRange(from = 0.0, to = 1.0) scale: Float = 1.0f,
@ColorInt colorFilter: Int = -1,
@@ -131,4 +137,4 @@ private fun Flashbar.Builder.withIcon(
iconColorFilter(colorFilter, colorFilterMode)
}
}
}
}
@@ -169,6 +169,7 @@
<string name="action_closeOthers">Close others</string>
<string name="action_closeThis">Close this</string>
<string name="all_saved">All files saved!</string>
<string name="save_failed">Failed to save files</string>
<string name="msg_newfile_dest">Destination: %s/</string>
<string name="restype_drawable">Drawable</string>
<string name="restype_layout">Layout</string>
@@ -539,4 +540,7 @@
<string name="idepref_deleteEmptyLines_summary">Whether the editor should delete empty lines on a single backspace key event.</string>
<string name="idepref_deleteTabs_title">Delete tab on backspace</string>
<string name="idepref_deleteTabs_summary">When enabled, the editor will delete spaces equal to the tab width when the backspace key is pressed.</string>
<string name="btn_sync">Sync</string>
<string name="btn_ignore_changes">Ignore these changes</string>
</resources>
@@ -652,7 +652,7 @@ class Flashbar private constructor(private var builder: Builder) {
RIGHT
}
interface OnActionTapListener {
fun interface OnActionTapListener {
fun onActionTapped(bar: Flashbar)
}