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 androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManager import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.resources.R import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess import com.itsaky.androidide.utils.flashSuccess
/** @author Akash Yadav */ /** @author Akash Yadav */
@@ -52,34 +54,40 @@ class SaveFileAction(context: Context) : EditorRelatedAction() {
enabled = context.areFilesModified() enabled = context.areFilesModified()
} }
override fun execAction(data: ActionData): Boolean { override fun execAction(data: ActionData): ResultWrapper {
val context = data.getActivity() ?: return false val context = data.getActivity() ?: return ResultWrapper()
return try { return try {
// Cannot use context.saveAll() because this.execAction is called on non-UI thread // Cannot use context.saveAll() because this.execAction is called on non-UI thread
// and saveAll call will result in UI actions // and saveAll call will result in UI actions
val result = context.saveAllResult() ResultWrapper(context.saveAllResult())
if (result.xmlSaved) {
ProjectManager.generateSources()
}
if (result.gradleSaved) {
context.notifySyncNeeded()
}
true
} catch (error: Throwable) { } catch (error: Throwable) {
log.error("Failed to save file", error) log.error("Failed to save file", error)
false ResultWrapper()
} }
} }
override fun postExec(data: ActionData, result: Any) { 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) flashSuccess(R.string.all_saved)
val saveResult = result.result
if (saveResult.xmlSaved) {
ProjectManager.generateSources()
}
if (saveResult.gradleSaved) {
context.notifySyncNeeded()
}
} else { } else {
log.error("Failed to save file") 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.ProjectManager.projectPath
import com.itsaky.androidide.projects.builder.BuildService import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.services.LogReceiver import com.itsaky.androidide.services.LogReceiver
import com.itsaky.androidide.ui.MaterialBanner
import com.itsaky.androidide.ui.editor.CodeEditorView import com.itsaky.androidide.ui.editor.CodeEditorView
import com.itsaky.androidide.uidesigner.UIDesignerActivity import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.utils.ActionMenuUtils.createMenu import com.itsaky.androidide.utils.ActionMenuUtils.createMenu
import com.itsaky.androidide.utils.ApkInstallationSessionCallback 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.DialogUtils.newMaterialDialogBuilder
import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.InstallationResultHandler.onResult import com.itsaky.androidide.utils.InstallationResultHandler.onResult
import com.itsaky.androidide.utils.flashError import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashbarBuilder
import com.itsaky.androidide.utils.resolveAttr 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.viewmodel.EditorViewModel
import com.itsaky.androidide.xml.resources.ResourceTableRegistry import com.itsaky.androidide.xml.resources.ResourceTableRegistry
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
@@ -191,13 +194,11 @@ abstract class BaseEditorActivity :
val packageName = onResult(this, intent) val packageName = onResult(this, intent)
if (packageName != null) { if (packageName != null) {
Snackbar.make(binding.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG) Snackbar.make(binding.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG)
.setAction(string.yes) { .setAction(string.yes) { tryLaunchApp(packageName) }
tryLaunchApp(packageName)
}
.show() .show()
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -385,13 +386,23 @@ abstract class BaseEditorActivity :
fun notifySyncNeeded(onConfirm: () -> Unit) { fun notifySyncNeeded(onConfirm: () -> Unit) {
val buildService = Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE) val buildService = Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE)
if (buildService != null && !buildService.isBuildInProgress) { if (buildService == null || buildService.isBuildInProgress) return
getSyncBanner()?.apply {
setNegative(android.R.string.cancel, null) flashbarBuilder(
setPositive(android.R.string.ok) { onConfirm() } duration = DURATION_INDEFINITE,
show() 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? { open fun getFileTreeFragment(): FileTreeFragment? {
@@ -406,7 +417,7 @@ abstract class BaseEditorActivity :
viewModel.statusText = text viewModel.statusText = text
viewModel.statusGravity = gravity viewModel.statusGravity = gravity
} }
private fun tryLaunchApp(packageName: String) { private fun tryLaunchApp(packageName: String) {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
launchIntent?.let { launchIntent?.let {
@@ -629,17 +640,6 @@ abstract class BaseEditorActivity :
builder.create().show() 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 { open fun installationSessionCallback(): SessionCallback {
return ApkInstallationSessionCallback(this).also { installationCallback = it } 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:layout_height="wrap_content"
android:indeterminate="true" /> 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 <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"
style="@style/AppTheme.TabLayout" style="@style/AppTheme.TabLayout"
+1 -1
View File
@@ -25,10 +25,10 @@ dependencies {
implementation(projects.eventbusEvents) implementation(projects.eventbusEvents)
implementation(projects.lexers) implementation(projects.lexers)
implementation(projects.resources) implementation(projects.resources)
implementation(projects.subprojects.flashbar)
api(projects.shared) api(projects.shared)
api(projects.logger) api(projects.logger)
api(projects.subprojects.flashbar)
testImplementation(libs.tests.junit) testImplementation(libs.tests.junit)
testImplementation(libs.tests.google.truth) 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.INFO
import com.itsaky.androidide.utils.FlashType.SUCCESS import com.itsaky.androidide.utils.FlashType.SUCCESS
const val LENGTH_SHORT = 2000L const val DURATION_SHORT = 2000L
const val LENGTH_LONG = 3500L const val DURATION_LONG = 3500L
const val DURATION_INDEFINITE = Flashbar.DURATION_INDEFINITE
private val COLOR_SUCCESS = Color.parseColor("#4CAF50") val COLOR_SUCCESS = Color.parseColor("#4CAF50")
private val COLOR_ERROR = Color.parseColor("#f44336") val COLOR_ERROR = Color.parseColor("#f44336")
private const val COLOR_INFO = Color.DKGRAY 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) return Flashbar.Builder(this)
.gravity(TOP) .gravity(gravity)
.duration(duration) .duration(duration)
.backgroundColor(resolveAttr(R.attr.colorPrimaryContainer)) .backgroundColor(backgroundColor)
.messageColor(resolveAttr(R.attr.colorOnPrimaryContainer)) .messageColor(messageColor)
} }
fun Activity.flashMessage(msg: String?, type: FlashType) { 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) 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) 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) return withIcon(R.drawable.ic_info, colorFilter = COLOR_INFO)
} }
private fun Flashbar.Builder.withIcon( fun Flashbar.Builder.withIcon(
@DrawableRes icon: Int, @DrawableRes icon: Int,
@FloatRange(from = 0.0, to = 1.0) scale: Float = 1.0f, @FloatRange(from = 0.0, to = 1.0) scale: Float = 1.0f,
@ColorInt colorFilter: Int = -1, @ColorInt colorFilter: Int = -1,
@@ -131,4 +137,4 @@ private fun Flashbar.Builder.withIcon(
iconColorFilter(colorFilter, colorFilterMode) iconColorFilter(colorFilter, colorFilterMode)
} }
} }
} }
@@ -169,6 +169,7 @@
<string name="action_closeOthers">Close others</string> <string name="action_closeOthers">Close others</string>
<string name="action_closeThis">Close this</string> <string name="action_closeThis">Close this</string>
<string name="all_saved">All files saved!</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="msg_newfile_dest">Destination: %s/</string>
<string name="restype_drawable">Drawable</string> <string name="restype_drawable">Drawable</string>
<string name="restype_layout">Layout</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_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_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="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> </resources>
@@ -652,7 +652,7 @@ class Flashbar private constructor(private var builder: Builder) {
RIGHT RIGHT
} }
interface OnActionTapListener { fun interface OnActionTapListener {
fun onActionTapped(bar: Flashbar) fun onActionTapped(bar: Flashbar)
} }