W.I.P: progress reader rewrite

This commit is contained in:
artdeell
2022-11-11 21:48:17 +03:00
committed by ArtDev
parent ac71ffbdef
commit dbfbbbc4bf
12 changed files with 336 additions and 102 deletions

View File

@@ -30,10 +30,8 @@
<activity
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:label="@string/app_short_name"
android:name=".LauncherActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation">
android:name=".LauncherActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -3,6 +3,7 @@ package com.kdt.mcgui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -16,8 +17,11 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.extra.ExtraCore;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.progresskeeper.ProgressListener;
import net.kdt.pojavlaunch.services.ProgressService;
import java.util.Arrays;
import java.util.ArrayList;
/** Class staring at specific values and automatically show something if the progress is present
@@ -47,88 +51,38 @@ public class ProgressLayout extends ConstraintLayout implements View.OnClickList
init();
}
private int mActiveProcesses = 0;
private final ArrayMap<String, TextProgressBar> mMap = new ArrayMap<>();
private final ArrayList<LayoutProgressListener> mMap = new ArrayList<>();
private LinearLayout mLinearLayout;
private TextView mTaskNumberDisplayer;
private ImageView mFlipArrow;
private final Runnable mCheckProgressRunnable = new Runnable() {
@Override
public void run() {
for(String progressKey : mMap.keySet()){
if(progressKey == null) continue; //TODO check wtf does this
Object object = ExtraCore.consumeValue(progressKey);
if(object != null){
String[] progressStuff = ((String) object).split("¤");
int progress = Integer.parseInt(progressStuff[0]);
int resourceString = Integer.parseInt(progressStuff[1]);
// Prepare the progressbar
if(mMap.get(progressKey) == null){
TextProgressBar textView = new TextProgressBar(getContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, getResources().getDimensionPixelOffset(R.dimen._20sdp));
params.bottomMargin = getResources().getDimensionPixelOffset(R.dimen._6sdp);
mLinearLayout.addView(textView, params);
mMap.put(progressKey, textView);
mActiveProcesses++;
}
mMap.get(progressKey).setProgress(progress);
if(resourceString != -1){
// As an optimization, copy array content back 2 indexes instead of creating another object
System.arraycopy(progressStuff, 2, progressStuff, 0, progressStuff.length - 2);
mMap.get(progressKey).setText(getResources().getString(resourceString, progressStuff));
}else{
if(progressStuff.length >= 3)
mMap.get(progressStuff[2]);
}
// Remove when we don't have progress
if(progress < 0){
if(progress <= -10) // Only remove the observer when it is explicitly told to do so ?
mMap.remove(progressKey);
mLinearLayout.removeView(mMap.get(progressKey));
mActiveProcesses--;
}
}
}
setVisibility(hasProcesses() ? VISIBLE : GONE);
mTaskNumberDisplayer.setText(getContext().getString(R.string.progresslayout_tasks_in_progress, mActiveProcesses));
postDelayed(this, 1000);
}
};
public void observe(String progressKey){
mMap.put(progressKey, null);
mMap.add(new LayoutProgressListener(progressKey));
}
public boolean hasProcesses(){
return mActiveProcesses > 0;
return ProgressKeeper.getTaskCount() > 0;
}
public boolean hasProcess(String process){
return mMap.get(process) != null;
}
private void init(){
inflate(getContext(), R.layout.view_progress, this);
mLinearLayout = findViewById(R.id.progress_linear_layout);
mTaskNumberDisplayer = findViewById(R.id.progress_textview);
mFlipArrow = findViewById(R.id.progress_flip_arrow);
postDelayed(mCheckProgressRunnable, 1000);
setBackgroundColor(getResources().getColor(R.color.background_bottom_bar));
setVisibility(GONE);
ProgressKeeper.addTaskCountListener((tc)->{
post(()->{
Log.i("ProgressLayout", "tc="+tc);
if(tc > 0) {
mTaskNumberDisplayer.setText(getContext().getString(R.string.progresslayout_tasks_in_progress, tc));
setVisibility(VISIBLE);
}else
setVisibility(GONE);
});
});
setOnClickListener(this);
}
@@ -138,13 +92,8 @@ public class ProgressLayout extends ConstraintLayout implements View.OnClickList
}
/** Update the text and progress content */
public static void setProgress(String progressKey, int progress, @StringRes int resource, String... message){
StringBuilder builder = new StringBuilder();
for(String bit : message){
builder.append(bit).append("¤");
}
ExtraCore.setValue(progressKey, progress + "¤" + resource + "¤" + builder);
public static void setProgress(String progressKey, int progress, @StringRes int resource, Object... message){
ProgressKeeper.submitProgress(progressKey, progress, resource, message);
}
/** Update the text and progress content */
@@ -162,4 +111,40 @@ public class ProgressLayout extends ConstraintLayout implements View.OnClickList
mLinearLayout.setVisibility(mLinearLayout.getVisibility() == GONE ? VISIBLE : GONE);
mFlipArrow.setRotation(mLinearLayout.getVisibility() == GONE? 0 : 180);
}
class LayoutProgressListener implements ProgressListener {
final String progressKey;
final TextProgressBar textView;
final LinearLayout.LayoutParams params;
public LayoutProgressListener(String progressKey) {
this.progressKey = progressKey;
textView = new TextProgressBar(getContext());
params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, getResources().getDimensionPixelOffset(R.dimen._20sdp));
params.bottomMargin = getResources().getDimensionPixelOffset(R.dimen._6sdp);
ProgressKeeper.addListener(progressKey, this);
}
@Override
public void onProgressStarted() {
post(()-> {
Log.i("ProgressLayout", "onProgressStarted");
mLinearLayout.addView(textView, params);
});
}
@Override
public void onProgressUpdated(int progress, int resid, Object... va) {
post(()-> {
textView.setProgress(progress);
if(resid != -1) textView.setText(getContext().getString(resid, va));
else textView.setText("");
});
}
@Override
public void onProgressEnded() {
post(()-> {
mLinearLayout.removeView(textView);
});
}
}
}

View File

@@ -41,6 +41,8 @@ import net.kdt.pojavlaunch.fragments.SelectAuthFragment;
import net.kdt.pojavlaunch.multirt.MultiRTConfigDialog;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.prefs.screens.LauncherPreferenceFragment;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.services.ProgressServiceKeeper;
import net.kdt.pojavlaunch.tasks.AsyncAssetManager;
import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader;
import net.kdt.pojavlaunch.tasks.AsyncVersionList;
@@ -60,6 +62,7 @@ public class LauncherActivity extends BaseActivity {
private FragmentContainerView mFragmentView;
private ImageButton mSettingsButton, mDeleteAccountButton;
private ProgressLayout mProgressLayout;
private ProgressServiceKeeper mProgressServiceKeeper;
/* Allows to switch from one button "type" to another */
private final FragmentManager.FragmentLifecycleCallbacks mFragmentCallbackListener = new FragmentManager.FragmentLifecycleCallbacks() {
@@ -151,7 +154,7 @@ public class LauncherActivity extends BaseActivity {
setContentView(R.layout.activity_pojav_launcher);
getWindow().setBackgroundDrawable(null);
bindViews();
ProgressKeeper.addTaskCountListener((mProgressServiceKeeper = new ProgressServiceKeeper(this)));
askForStoragePermission(); // Will wait here
mSettingsButton.setOnClickListener(mSettingButtonListener);

View File

@@ -0,0 +1,98 @@
package net.kdt.pojavlaunch.progresskeeper;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ProgressKeeper {
private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<WeakReference<ProgressListener>>> sProgressListeners = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, ProgressState> sProgressStates = new ConcurrentHashMap<>();
private static final ArrayList<WeakReference<TaskCountListener>> sTaskCountListeners = new ArrayList<>();
public static void submitProgress(String progressRecord, int progress, int resid, Object... va) {
ProgressState progressState = sProgressStates.get(progressRecord);
boolean shouldCallStarted = progressState == null;
boolean shouldCallEnded = resid == -1 && progress == -1;
if(shouldCallEnded) {
shouldCallStarted = false;
sProgressStates.remove(progressRecord);
updateTaskCount();
}else if(shouldCallStarted){
sProgressStates.put(progressRecord, (progressState = new ProgressState()));
updateTaskCount();
}
if(progressState != null) {
progressState.progress = progress;
progressState.resid = resid;
progressState.varArg = va;
}
Log.d("ProgressLayout", "shouldCallStarted="+shouldCallStarted+" shouldCallEnded="+shouldCallEnded);
ConcurrentLinkedQueue<WeakReference<ProgressListener>> listenerWeakReferenceList = sProgressListeners.get(progressRecord);
if(listenerWeakReferenceList != null) {
Iterator<WeakReference<ProgressListener>> iterator = listenerWeakReferenceList.iterator();
while(iterator.hasNext()) {
ProgressListener listener = iterator.next().get();
Log.i("ProgressLayout", listener+"");
if(listener != null) {
if(shouldCallStarted) listener.onProgressStarted();
else if(shouldCallEnded) listener.onProgressEnded();
else listener.onProgressUpdated(progress, resid, va);
} else iterator.remove();
}
}
}
private static void updateTaskCount() {
Log.i("ProgressKeeper","updateTaskCount()");
int count = sProgressStates.size();
Iterator<WeakReference<TaskCountListener>> taskCountListeners = sTaskCountListeners.iterator();
while(taskCountListeners.hasNext()) {
TaskCountListener countListener = taskCountListeners.next().get();
if(countListener == null) {
Log.i("ProgressKeeper","Swooped a listener");
taskCountListeners.remove();
}
else countListener.onUpdateTaskCount(count);
}
}
public static void addListener(String progressRecord, ProgressListener listener) {
ProgressState state = sProgressStates.get(progressRecord);
if(state != null && (state.resid != -1 || state.progress != -1)) {
listener.onProgressStarted();
listener.onProgressUpdated(state.progress, state.resid, state.varArg);
Log.d("ProgressLayout", "Resubmitting UI state");
}else{
listener.onProgressEnded();
}
ConcurrentLinkedQueue<WeakReference<ProgressListener>> listenerWeakReferenceList = sProgressListeners.get(progressRecord);
if(listenerWeakReferenceList == null) sProgressListeners.put(progressRecord, (listenerWeakReferenceList = new ConcurrentLinkedQueue<>()));
else {
Iterator<WeakReference<ProgressListener>> iterator = listenerWeakReferenceList.iterator();
while(iterator.hasNext()) {
if(iterator.next().get() == null) iterator.remove();
}
}
listenerWeakReferenceList.add(new WeakReference<>(listener));
}
public static void addTaskCountListener(TaskCountListener listener) {
Iterator<WeakReference<TaskCountListener>> taskCountListeners = sTaskCountListeners.iterator();
while(taskCountListeners.hasNext()) {
TaskCountListener countListener = taskCountListeners.next().get();
if(countListener == null){
Log.i("ProgressKeeper","Swooped a listener");
taskCountListeners.remove();
}
}
listener.onUpdateTaskCount(sProgressStates.size());
sTaskCountListeners.add(new WeakReference<>(listener));
}
public static int getTaskCount() {
return sProgressStates.size();
}
}

View File

@@ -0,0 +1,7 @@
package net.kdt.pojavlaunch.progresskeeper;
public interface ProgressListener {
void onProgressStarted();
void onProgressUpdated(int progress, int resid, Object... va);
void onProgressEnded();
}

View File

@@ -0,0 +1,7 @@
package net.kdt.pojavlaunch.progresskeeper;
public class ProgressState {
int progress;
int resid;
Object[] varArg;
}

View File

@@ -0,0 +1,5 @@
package net.kdt.pojavlaunch.progresskeeper;
public interface TaskCountListener {
void onUpdateTaskCount(int taskCount);
}

View File

@@ -5,54 +5,49 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.progresskeeper.ProgressListener;
import net.kdt.pojavlaunch.progresskeeper.TaskCountListener;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Lazy service which allows the process not to get killed.
* Can be created from context, can be killed statically
*/
public class ProgressService extends Service {
public class ProgressService extends Service implements TaskCountListener {
private static WeakReference<Service> sProgressService = new WeakReference<>(null);
private static final AtomicInteger sReferenceCount = new AtomicInteger(0);
private Handler mainThreadHandler = new Handler();
private NotificationManagerCompat notificationManagerCompat;
/** Simple wrapper to start the service */
public static void startService(Context context){
Intent intent = new Intent(context, ProgressService.class);
if(sReferenceCount.get() < 0) sReferenceCount.set(0);
sReferenceCount.getAndIncrement();
ContextCompat.startForegroundService(context, intent);
}
/** Kill the service if it is still running */
public static void killService(){
Service service = sProgressService.get();
int refcnt = sReferenceCount.decrementAndGet();
if(service != null && refcnt <= 0) {
service.stopSelf();
}
}
private NotificationCompat.Builder mNotificationBuilder;
public ProgressService(){
super();
sProgressService = new WeakReference<>(this);
}
@Override
public void onCreate() {
Tools.buildNotificationChannel(getApplicationContext());
notificationManagerCompat = NotificationManagerCompat.from(getApplicationContext());
Intent killIntent = new Intent(getApplicationContext(), ProgressService.class);
killIntent.putExtra("kill", true);
PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
@@ -60,16 +55,20 @@ public class ProgressService extends Service {
.setContentTitle(getString(R.string.lazy_service_default_title))
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent)
.setSmallIcon(R.mipmap.ic_launcher_round);
ProgressKeeper.addTaskCountListener(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent != null && intent.getBooleanExtra("kill", false)) {
stopSelf(); // otherwise Android tries to restart the service since it "crashed"
Process.killProcess(Process.myPid());
return super.onStartCommand(intent, flags, startId);
if(intent != null) {
if(intent.getBooleanExtra("kill", false)) {
stopSelf(); // otherwise Android tries to restart the service since it "crashed"
Process.killProcess(Process.myPid());
return super.onStartCommand(intent, flags, startId);
}
}
mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, sReferenceCount.get()));
Log.d("ProgressService", "Started!");
mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, ProgressKeeper.getTaskCount()));
startForeground(1,mNotificationBuilder.build());
return super.onStartCommand(intent, flags, startId);
}
@@ -79,4 +78,16 @@ public class ProgressService extends Service {
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onUpdateTaskCount(int taskCount) {
mainThreadHandler.post(()->{
if(taskCount < 0) {
mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, taskCount));
notificationManagerCompat.notify(1, mNotificationBuilder.build());
}else{
stopSelf();
}
});
}
}

View File

@@ -0,0 +1,19 @@
package net.kdt.pojavlaunch.services;
import android.content.Context;
import android.util.Log;
import net.kdt.pojavlaunch.progresskeeper.TaskCountListener;
public class ProgressServiceKeeper implements TaskCountListener {
private Context context;
public ProgressServiceKeeper(Context ctx) {
this.context = ctx;
}
@Override
public void onUpdateTaskCount(int taskCount) {
Log.d("ProgressServiceKeeper", taskCount+"");
if(taskCount > 0) ProgressService.startService(context);
}
}

View File

@@ -54,6 +54,7 @@ public class AsyncAssetManager {
// Install the runtime in an async manner, hope for the best
String finalRt_version = rt_version;
sExecutorService.execute(() -> {
try {
MultiRTUtils.installRuntimeNamedBinpack(
am.open("components/jre/universal.tar.xz"),

View File

@@ -47,13 +47,9 @@ public class AsyncMinecraftDownloader {
public AsyncMinecraftDownloader(@NonNull Activity activity, JMinecraftVersionList.Version version,
@NonNull DoneListener listener){
ProgressService.startService(activity);
sExecutorService.execute(() -> {
if(downloadGame(activity, version, version.id))
listener.onDownloadDone();
ProgressService.killService();
});
}

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_menu_main"
android:gravity="top"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@color/background_app"
>
<com.kdt.mcgui.LauncherMenuButton
android:id="@+id/news_button"
android:layout_width="match_parent"
android:layout_height="@dimen/_66sdp"
android:background="?android:attr/selectableItemBackground"
android:text="@string/mcl_tab_news"
android:drawableStart="@drawable/ic_menu_news"
app:layout_constraintTop_toTopOf="parent"
/>
<com.kdt.mcgui.LauncherMenuButton
android:id="@+id/custom_control_button"
android:layout_width="match_parent"
android:layout_height="@dimen/_66sdp"
android:background="?android:attr/selectableItemBackground"
android:drawableStart="@drawable/ic_menu_custom_controls"
android:text="@string/mcl_option_customcontrol"
app:layout_constraintTop_toBottomOf="@id/news_button"
/>
<com.kdt.mcgui.LauncherMenuButton
android:id="@+id/install_jar_button"
android:layout_width="match_parent"
android:layout_height="@dimen/_66sdp"
android:background="?android:attr/selectableItemBackground"
android:drawableStart="@drawable/ic_menu_install_jar"
android:text="@string/main_install_jar_file"
app:layout_constraintTop_toBottomOf="@id/custom_control_button"
/>
<com.kdt.mcgui.mcVersionSpinner
android:id="@+id/mc_version_spinner"
android:layout_width="0dp"
android:layout_height="@dimen/_32sdp"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:drawableEnd="@drawable/spinner_arrow"
app:drawableEndPadding="@dimen/_1sdp"
app:drawableEndSize="@dimen/_12sdp"
app:drawableStartIntegerScaling="true"
app:drawableStartSize="@dimen/_36sdp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/edit_profile_button"
app:layout_constraintStart_toStartOf="parent" />
<ImageButton
android:id="@+id/edit_profile_button"
android:layout_width="@dimen/_32sdp"
android:layout_height="0dp"
android:layout_marginEnd="@dimen/_8sdp"
android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_edit_profile"
app:layout_constraintBottom_toBottomOf="@+id/mc_version_spinner"
app:layout_constraintEnd_toStartOf="@+id/play_button"
app:layout_constraintTop_toTopOf="@+id/mc_version_spinner" />
<!-- This view is for cosmetic purpose only -->
<View
android:id="@+id/_background_display_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="-8dp"
android:background="@color/background_bottom_bar"
android:translationZ="-1dp"
app:layout_constraintTop_toTopOf="@id/mc_version_spinner" />
<com.kdt.mcgui.MineButton
android:id="@+id/play_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/_8sdp"
android:text="@string/main_play"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="@+id/mc_version_spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/mc_version_spinner" />
</androidx.constraintlayout.widget.ConstraintLayout>