fdroid-client/app/src/full/java/org/fdroid/fdroid/nearby/SwapSuccessView.java

419 lines
18 KiB
Java

package org.fdroid.fdroid.nearby;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.cursoradapter.widget.CursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.bumptech.glide.Glide;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.download.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import java.util.List;
/**
* This is a view that shows a listing of all apps in the swap repo that this
* just connected to. The app listing and search should be replaced by
* {@link org.fdroid.fdroid.views.apps.AppListActivity}'s plumbing.
*/
// TODO merge this with AppListActivity, perhaps there could be AppListView?
public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = "SwapAppsView";
public SwapSuccessView(Context context) {
super(context);
}
public SwapSuccessView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwapSuccessView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public SwapSuccessView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private Repo repo;
private AppListAdapter adapter;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
repo = getActivity().getSwapService().getPeerRepo();
adapter = new AppListAdapter(getContext(), getContext().getContentResolver().query(
AppProvider.getRepoUri(repo), AppMetadataTable.Cols.ALL, null, null, null));
ListView listView = findViewById(R.id.list);
listView.setAdapter(adapter);
// either reconnect with an existing loader or start a new one
getActivity().getSupportLoaderManager().initLoader(R.layout.swap_success, null, this);
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
pollForUpdatesReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));
}
/**
* Remove relevant listeners/receivers/etc so that they do not receive and process events
* when this view is not in use.
*/
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(pollForUpdatesReceiver);
}
@NonNull
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri uri = TextUtils.isEmpty(currentFilterString)
? AppProvider.getRepoUri(repo)
: AppProvider.getSearchUri(repo, currentFilterString);
return new CursorLoader(getActivity(), uri, AppMetadataTable.Cols.ALL,
null, null, AppMetadataTable.Cols.NAME);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
adapter.swapCursor(null);
}
private class AppListAdapter extends CursorAdapter {
private class ViewHolder {
private final LocalBroadcastManager localBroadcastManager;
@Nullable
private App app;
@Nullable
private Apk apk;
ProgressBar progressView;
TextView nameView;
ImageView iconView;
Button btnInstall;
TextView statusInstalled;
TextView statusIncompatible;
private class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case DownloaderService.ACTION_STARTED:
resetView();
break;
case DownloaderService.ACTION_PROGRESS:
if (progressView.getVisibility() != View.VISIBLE) {
showProgress();
}
long read = intent.getLongExtra(DownloaderService.EXTRA_BYTES_READ, 0);
long total = intent.getLongExtra(DownloaderService.EXTRA_TOTAL_BYTES, 0);
if (total > 0) {
progressView.setIndeterminate(false);
progressView.setMax(100);
progressView.setProgress(Utils.getPercent(read, total));
} else {
progressView.setIndeterminate(true);
}
break;
case DownloaderService.ACTION_COMPLETE:
localBroadcastManager.unregisterReceiver(this);
resetView();
statusInstalled.setText(R.string.installing);
statusInstalled.setVisibility(View.VISIBLE);
btnInstall.setVisibility(View.GONE);
break;
case DownloaderService.ACTION_INTERRUPTED:
localBroadcastManager.unregisterReceiver(this);
if (intent.hasExtra(DownloaderService.EXTRA_ERROR_MESSAGE)) {
String msg = intent.getStringExtra(DownloaderService.EXTRA_ERROR_MESSAGE)
+ " " + intent.getDataString();
Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
} else { // user canceled
Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
}
resetView();
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
}
private final ContentObserver appObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
AppCompatActivity activity = getActivity();
if (activity != null && app != null) {
app = AppProvider.Helper.findSpecificApp(
activity.getContentResolver(),
app.packageName,
app.repoId,
AppMetadataTable.Cols.ALL);
resetView();
}
}
};
ViewHolder() {
localBroadcastManager = LocalBroadcastManager.getInstance(getContext());
}
public void setApp(@NonNull App app) {
if (this.app == null || !this.app.packageName.equals(app.packageName)) {
this.app = app;
List<Apk> availableApks = ApkProvider.Helper.findAppVersionsByRepo(getActivity(), app, repo);
if (availableApks.size() > 0) {
// Swap repos only add one version of an app, so we will just ask for the first apk.
this.apk = availableApks.get(0);
}
if (apk != null) {
localBroadcastManager.registerReceiver(new DownloadReceiver(),
DownloaderService.getIntentFilter(apk.getCanonicalUrl()));
localBroadcastManager.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
statusInstalled.setText(R.string.installing);
statusInstalled.setVisibility(View.VISIBLE);
btnInstall.setVisibility(View.GONE);
progressView.setIndeterminate(true);
progressView.setVisibility(View.VISIBLE);
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
case Installer.ACTION_INSTALL_COMPLETE:
localBroadcastManager.unregisterReceiver(this);
statusInstalled.setText(R.string.app_installed);
statusInstalled.setVisibility(View.VISIBLE);
btnInstall.setVisibility(View.GONE);
progressView.setVisibility(View.GONE);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
localBroadcastManager.unregisterReceiver(this);
statusInstalled.setVisibility(View.GONE);
btnInstall.setVisibility(View.VISIBLE);
progressView.setVisibility(View.GONE);
String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (errorMessage != null) {
Toast.makeText(getContext(), errorMessage, Toast.LENGTH_LONG).show();
}
break;
}
}
}, Installer.getInstallIntentFilter(apk.getCanonicalUrl()));
}
// NOTE: Instead of continually unregistering and re-registering the observer
// (with a different URI), this could equally be done by only having one
// registration in the constructor, and using the ContentObserver.onChange(boolean, URI)
// method and inspecting the URI to see if it matches. However, this was only
// implemented on API-16, so leaving like this for now.
getActivity().getContentResolver().unregisterContentObserver(appObserver);
getActivity().getContentResolver().registerContentObserver(
AppProvider.getSpecificAppUri(this.app.packageName, this.app.repoId), true, appObserver);
}
resetView();
}
private final OnClickListener cancelListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (apk != null) {
InstallManagerService.cancel(getContext(), apk.getCanonicalUrl());
}
}
};
private final OnClickListener installListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (apk != null && (app.hasUpdates() || app.compatible)) {
showProgress();
InstallManagerService.queue(getContext(), app, apk);
}
}
};
private void resetView() {
if (app == null) {
return;
}
progressView.setVisibility(View.GONE);
progressView.setIndeterminate(true);
if (app.name != null) {
nameView.setText(app.name);
}
app.loadWithGlide(iconView.getContext())
.apply(Utils.getAlwaysShowIconRequestOptions())
.into(iconView);
if (app.hasUpdates()) {
btnInstall.setText(R.string.menu_upgrade);
btnInstall.setVisibility(View.VISIBLE);
btnInstall.setOnClickListener(installListener);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
} else if (app.isInstalled(getContext())) {
btnInstall.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.VISIBLE);
statusInstalled.setText(R.string.app_installed);
} else if (!app.compatible) {
btnInstall.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.VISIBLE);
statusInstalled.setVisibility(View.GONE);
} else if (progressView.getVisibility() == View.VISIBLE) {
btnInstall.setText(R.string.cancel);
btnInstall.setVisibility(View.VISIBLE);
btnInstall.setOnClickListener(cancelListener);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
} else {
btnInstall.setText(R.string.menu_install);
btnInstall.setVisibility(View.VISIBLE);
btnInstall.setOnClickListener(installListener);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
}
}
private void showProgress() {
btnInstall.setText(R.string.cancel);
btnInstall.setVisibility(View.VISIBLE);
btnInstall.setOnClickListener(cancelListener);
progressView.setVisibility(View.VISIBLE);
statusInstalled.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.GONE);
}
}
@Nullable
private LayoutInflater inflater;
AppListAdapter(@NonNull Context context, @Nullable Cursor c) {
super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
}
@NonNull
private LayoutInflater getInflater(Context context) {
if (inflater == null) {
inflater = ContextCompat.getSystemService(context, LayoutInflater.class);
}
return inflater;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = getInflater(context).inflate(R.layout.swap_app_list_item, parent, false);
ViewHolder holder = new ViewHolder();
holder.progressView = (ProgressBar) view.findViewById(R.id.progress);
holder.nameView = (TextView) view.findViewById(R.id.name);
holder.iconView = (ImageView) view.findViewById(android.R.id.icon);
holder.btnInstall = (Button) view.findViewById(R.id.btn_install);
holder.statusInstalled = (TextView) view.findViewById(R.id.status_installed);
holder.statusIncompatible = (TextView) view.findViewById(R.id.status_incompatible);
view.setTag(holder);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
ViewHolder holder = (ViewHolder) view.getTag();
final App app = new App(cursor);
holder.setApp(app);
}
}
private final BroadcastReceiver pollForUpdatesReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int statusCode = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1);
switch (statusCode) {
case UpdateService.STATUS_COMPLETE_WITH_CHANGES:
Utils.debugLog(TAG, "Swap repo has updates, notifying the list adapter.");
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
});
break;
}
}
};
}