Merge branch 'big-install-process-event-overhaul' into 'master'

Big install process event overhaul

Closes #1357

See merge request fdroid/fdroidclient!717
This commit is contained in:
Hans-Christoph Steiner 2018-07-20 22:55:23 +00:00
commit 6876088ede
9 changed files with 78 additions and 68 deletions

View File

@ -362,7 +362,7 @@ public class AppDetails2 extends AppCompatActivity
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData(); Uri uri = data.getData();
Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL); Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL);
startInstall(apk); InstallManagerService.queue(this, app, apk);
} }
break; break;
case REQUEST_UNINSTALL_DIALOG: case REQUEST_UNINSTALL_DIALOG:
@ -373,6 +373,12 @@ public class AppDetails2 extends AppCompatActivity
} }
} }
@Override
public void installApk() {
Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app);
installApk(apkToInstall);
}
// Install the version of this app denoted by 'app.curApk'. // Install the version of this app denoted by 'app.curApk'.
@Override @Override
public void installApk(final Apk apk) { public void installApk(final Apk apk) {
@ -426,12 +432,6 @@ public class AppDetails2 extends AppCompatActivity
} }
private void initiateInstall(Apk apk) { private void initiateInstall(Apk apk) {
if (isAppDownloading()) {
Log.i(TAG, "Ignoring request to install " + apk.packageName + " version " + apk.versionName
+ ", as we are already downloading (either that or a different version).");
return;
}
Installer installer = InstallerFactory.create(this, apk); Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getPermissionScreen(); Intent intent = installer.getPermissionScreen();
if (intent != null) { if (intent != null) {
@ -441,10 +441,6 @@ public class AppDetails2 extends AppCompatActivity
return; return;
} }
startInstall(apk);
}
private void startInstall(Apk apk) {
InstallManagerService.queue(this, app, apk); InstallManagerService.queue(this, app, apk);
} }
@ -485,6 +481,7 @@ public class AppDetails2 extends AppCompatActivity
} }
switch (newStatus.status) { switch (newStatus.status) {
case PendingInstall:
case Downloading: case Downloading:
if (newStatus.progressMax == 0) { if (newStatus.progressMax == 0) {
// The first progress notification we get telling us our status is "Downloading" // The first progress notification we get telling us our status is "Downloading"
@ -708,11 +705,6 @@ public class AppDetails2 extends AppCompatActivity
}); });
} }
@Override
public boolean isAppDownloading() {
return currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Downloading;
}
@Override @Override
public void enableAndroidBeam() { public void enableAndroidBeam() {
NfcHelper.setAndroidBeam(this, app.packageName); NfcHelper.setAndroidBeam(this, app.packageName);
@ -737,7 +729,7 @@ public class AppDetails2 extends AppCompatActivity
@Override @Override
public void installCancel() { public void installCancel() {
if (isAppDownloading()) { if (currentStatus != null) {
InstallManagerService.cancel(this, currentStatus.getUniqueKey()); InstallManagerService.cancel(this, currentStatus.getUniqueKey());
} }
} }
@ -753,18 +745,6 @@ public class AppDetails2 extends AppCompatActivity
} }
} }
@Override
public void installApk() {
Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app);
installApk(apkToInstall);
}
@Override
public void upgradeApk() {
Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app);
installApk(apkToInstall);
}
/** /**
* Uninstall the app from the current screen. Since there are many ways * Uninstall the app from the current screen. Since there are many ways
* to uninstall an app, including from Google Play, {@code adb uninstall}, * to uninstall an app, including from Google Play, {@code adb uninstall},

View File

@ -91,13 +91,14 @@ public final class AppUpdateStatusManager {
private static final String LOGTAG = "AppUpdateStatusManager"; private static final String LOGTAG = "AppUpdateStatusManager";
public enum Status { public enum Status {
PendingInstall,
DownloadInterrupted, DownloadInterrupted,
UpdateAvailable, UpdateAvailable,
Downloading, Downloading,
ReadyToInstall, ReadyToInstall,
Installing, Installing,
Installed, Installed,
InstallError InstallError,
} }
public static AppUpdateStatusManager getInstance(Context context) { public static AppUpdateStatusManager getInstance(Context context) {
@ -426,6 +427,7 @@ public final class AppUpdateStatusManager {
entry.errorText = errorText; entry.errorText = errorText;
entry.intent = null; entry.intent = null;
notifyChange(entry, true); notifyChange(entry, true);
removeApk(url);
} }
} }
} }

View File

@ -230,6 +230,7 @@ class NotificationHelper {
case UpdateAvailable: case UpdateAvailable:
return new NotificationCompat.Action(R.drawable.ic_file_download, context.getString(R.string.notification_action_update), entry.intent); return new NotificationCompat.Action(R.drawable.ic_file_download, context.getString(R.string.notification_action_update), entry.intent);
case PendingInstall:
case Downloading: case Downloading:
case Installing: case Installing:
return new NotificationCompat.Action(R.drawable.ic_cancel, context.getString(R.string.notification_action_cancel), entry.intent); return new NotificationCompat.Action(R.drawable.ic_cancel, context.getString(R.string.notification_action_cancel), entry.intent);
@ -245,6 +246,7 @@ class NotificationHelper {
switch (status) { switch (status) {
case UpdateAvailable: case UpdateAvailable:
return context.getString(R.string.notification_title_single_update_available); return context.getString(R.string.notification_title_single_update_available);
case PendingInstall:
case Downloading: case Downloading:
return app.name; return app.name;
case ReadyToInstall: case ReadyToInstall:
@ -263,6 +265,7 @@ class NotificationHelper {
switch (status) { switch (status) {
case UpdateAvailable: case UpdateAvailable:
return app.name; return app.name;
case PendingInstall:
case Downloading: case Downloading:
return context.getString(app.isInstalled(context) ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name); return context.getString(app.isInstalled(context) ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name);
case ReadyToInstall: case ReadyToInstall:
@ -281,6 +284,7 @@ class NotificationHelper {
switch (status) { switch (status) {
case UpdateAvailable: case UpdateAvailable:
return context.getString(R.string.notification_title_summary_update_available); return context.getString(R.string.notification_title_summary_update_available);
case PendingInstall:
case Downloading: case Downloading:
return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading); return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading);
case ReadyToInstall: case ReadyToInstall:

View File

@ -145,7 +145,6 @@ public class FileInstallerActivity extends FragmentActivity {
private void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) { private void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) {
Utils.debugLog(TAG, "Installing: " + localApkUri.getPath()); Utils.debugLog(TAG, "Installing: " + localApkUri.getPath());
installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED);
File path = apk.getMediaInstallPath(activity.getApplicationContext()); File path = apk.getMediaInstallPath(activity.getApplicationContext());
path.mkdirs(); path.mkdirs();
try { try {

View File

@ -34,7 +34,13 @@ import java.io.IOException;
/** /**
* Manages the whole process when a background update triggers an install or the user * Manages the whole process when a background update triggers an install or the user
* requests an APK to be installed. It handles checking whether the APK is cached, * requests an APK to be installed. It handles checking whether the APK is cached,
* downloading it, putting up and maintaining a {@link Notification}, and more. * downloading it, putting up and maintaining a {@link Notification}, and more. This
* {@code Service} tracks packages that are in the process as "Pending Installs".
* Then {@link DownloaderService} and {@link InstallerService} individually track
* packages for those phases of the whole install process. Each of those
* {@code Services} have their own related events. For tracking status during the
* whole process, {@link AppUpdateStatusManager} tracks the status as represented by
* {@link AppUpdateStatusManager.AppUpdateStatus}.
* <p> * <p>
* The {@link App} and {@link Apk} instances are sent via * The {@link App} and {@link Apk} instances are sent via
* {@link Intent#putExtra(String, android.os.Bundle)} * {@link Intent#putExtra(String, android.os.Bundle)}
@ -103,7 +109,6 @@ public class InstallManagerService extends Service {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Utils.debugLog(TAG, "creating Service");
localBroadcastManager = LocalBroadcastManager.getInstance(this); localBroadcastManager = LocalBroadcastManager.getInstance(this);
appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); appUpdateStatusManager = AppUpdateStatusManager.getInstance(this);
@ -171,7 +176,6 @@ public class InstallManagerService extends Service {
DownloaderService.cancel(this, apk.getPatchObbUrl()); DownloaderService.cancel(this, apk.getPatchObbUrl());
DownloaderService.cancel(this, apk.getMainObbUrl()); DownloaderService.cancel(this, apk.getMainObbUrl());
} }
appUpdateStatusManager.removeApk(urlString);
return START_NOT_STICKY; return START_NOT_STICKY;
} else if (ACTION_INSTALL.equals(action)) { } else if (ACTION_INSTALL.equals(action)) {
if (!isPendingInstall(urlString)) { if (!isPendingInstall(urlString)) {
@ -455,14 +459,12 @@ public class InstallManagerService extends Service {
*/ */
public static void queue(Context context, App app, @NonNull Apk apk) { public static void queue(Context context, App app, @NonNull Apk apk) {
String urlString = apk.getUrl(); String urlString = apk.getUrl();
AppUpdateStatusManager.getInstance(context).addApk(apk, AppUpdateStatusManager.Status.PendingInstall, null);
putPendingInstall(context, urlString, apk.packageName); putPendingInstall(context, urlString, apk.packageName);
Uri downloadUri = Uri.parse(urlString);
Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk,
null, null);
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString); Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString);
Intent intent = new Intent(context, InstallManagerService.class); Intent intent = new Intent(context, InstallManagerService.class);
intent.setAction(ACTION_INSTALL); intent.setAction(ACTION_INSTALL);
intent.setData(downloadUri); intent.setData(Uri.parse(urlString));
intent.putExtra(EXTRA_APP, app); intent.putExtra(EXTRA_APP, app);
intent.putExtra(EXTRA_APK, apk); intent.putExtra(EXTRA_APK, apk);
context.startService(intent); context.startService(intent);
@ -487,12 +489,25 @@ public class InstallManagerService extends Service {
return pendingInstalls.contains(urlString); return pendingInstalls.contains(urlString);
} }
/**
* Look up by {@code packageName} whether it is a Pending Install.
*
* @see #isPendingInstall(String)
*/
public static boolean isPendingInstall(Context context, String packageName) {
if (pendingInstalls == null) {
pendingInstalls = getPendingInstalls(context);
}
return pendingInstalls.getAll().values().contains(packageName);
}
/** /**
* Mark a given APK as in the process of being installed, with * Mark a given APK as in the process of being installed, with
* the {@code urlString} of the download used as the unique ID, * the {@code urlString} of the download used as the unique ID,
* and the file hash used to verify that things are the same. * and the file hash used to verify that things are the same.
* *
* @see #isPendingInstall(String) * @see #isPendingInstall(String)
* @see #isPendingInstall(Context, String)
*/ */
public static void putPendingInstall(Context context, String urlString, String packageName) { public static void putPendingInstall(Context context, String urlString, String packageName) {
if (pendingInstalls == null) { if (pendingInstalls == null) {

View File

@ -115,6 +115,8 @@ public class InstallerService extends JobIntentService {
* @see #uninstall(Context, Apk) * @see #uninstall(Context, Apk)
*/ */
public static void install(Context context, Uri localApkUri, Uri downloadUri, Apk apk) { public static void install(Context context, Uri localApkUri, Uri downloadUri, Apk apk) {
Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk,
null, null);
Intent intent = new Intent(context, InstallerService.class); Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL); intent.setAction(ACTION_INSTALL);
intent.setData(localApkUri); intent.setData(localApkUri);

View File

@ -43,6 +43,7 @@ import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.privileged.views.AppDiff; import org.fdroid.fdroid.privileged.views.AppDiff;
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions; import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
import org.fdroid.fdroid.views.main.MainActivity; import org.fdroid.fdroid.views.main.MainActivity;
@ -57,8 +58,6 @@ public class AppDetailsRecyclerViewAdapter
public interface AppDetailsRecyclerViewAdapterCallbacks { public interface AppDetailsRecyclerViewAdapterCallbacks {
boolean isAppDownloading();
void enableAndroidBeam(); void enableAndroidBeam();
void disableAndroidBeam(); void disableAndroidBeam();
@ -69,8 +68,6 @@ public class AppDetailsRecyclerViewAdapter
void installApk(Apk apk); void installApk(Apk apk);
void upgradeApk();
void uninstallApk(); void uninstallApk();
void installCancel(); void installCancel();
@ -378,11 +375,17 @@ public class AppDetailsRecyclerViewAdapter
progressLabel.setText(resIdString); progressLabel.setText(resIdString);
progressLabel.setContentDescription(context.getString(R.string.downloading)); progressLabel.setContentDescription(context.getString(R.string.downloading));
progressPercent.setText(""); progressPercent.setText("");
if (resIdString == R.string.installing || resIdString == R.string.uninstalling) {
progressCancel.setVisibility(View.GONE);
} else {
progressCancel.setVisibility(View.VISIBLE);
}
} }
public void setProgress(long bytesDownloaded, long totalBytes) { public void setProgress(long bytesDownloaded, long totalBytes) {
progressLayout.setVisibility(View.VISIBLE); progressLayout.setVisibility(View.VISIBLE);
buttonLayout.setVisibility(View.GONE); buttonLayout.setVisibility(View.GONE);
progressCancel.setVisibility(View.VISIBLE);
progressBar.setMax(Utils.bytesToKb(totalBytes)); progressBar.setMax(Utils.bytesToKb(totalBytes));
progressBar.setProgress(Utils.bytesToKb(bytesDownloaded)); progressBar.setProgress(Utils.bytesToKb(bytesDownloaded));
@ -484,9 +487,11 @@ public class AppDetailsRecyclerViewAdapter
buttonSecondaryView.setOnClickListener(onUnInstallClickListener); buttonSecondaryView.setOnClickListener(onUnInstallClickListener);
buttonPrimaryView.setText(R.string.menu_install); buttonPrimaryView.setText(R.string.menu_install);
buttonPrimaryView.setVisibility(versions.size() > 0 ? View.VISIBLE : View.GONE); buttonPrimaryView.setVisibility(versions.size() > 0 ? View.VISIBLE : View.GONE);
if (callbacks.isAppDownloading()) { if (InstallManagerService.isPendingInstall(context, app.packageName)) {
buttonPrimaryView.setText(R.string.downloading); buttonPrimaryView.setText(R.string.downloading);
buttonPrimaryView.setEnabled(false); buttonPrimaryView.setEnabled(false);
buttonLayout.setVisibility(View.GONE);
progressLayout.setVisibility(View.VISIBLE);
} else if (!app.isInstalled(context) && suggestedApk != null) { } else if (!app.isInstalled(context) && suggestedApk != null) {
// Check count > 0 due to incompatible apps resulting in an empty list. // Check count > 0 due to incompatible apps resulting in an empty list.
callbacks.disableAndroidBeam(); callbacks.disableAndroidBeam();
@ -494,6 +499,8 @@ public class AppDetailsRecyclerViewAdapter
buttonPrimaryView.setText(R.string.menu_install); buttonPrimaryView.setText(R.string.menu_install);
buttonPrimaryView.setOnClickListener(onInstallClickListener); buttonPrimaryView.setOnClickListener(onInstallClickListener);
buttonPrimaryView.setEnabled(true); buttonPrimaryView.setEnabled(true);
buttonLayout.setVisibility(View.VISIBLE);
progressLayout.setVisibility(View.GONE);
} else if (app.isInstalled(context)) { } else if (app.isInstalled(context)) {
callbacks.enableAndroidBeam(); callbacks.enableAndroidBeam();
if (app.canAndWantToUpdate(context) && suggestedApk != null) { if (app.canAndWantToUpdate(context) && suggestedApk != null) {
@ -508,10 +515,8 @@ public class AppDetailsRecyclerViewAdapter
} }
} }
buttonPrimaryView.setEnabled(true); buttonPrimaryView.setEnabled(true);
} buttonLayout.setVisibility(View.VISIBLE);
if (callbacks.isAppDownloading()) { progressLayout.setVisibility(View.GONE);
buttonLayout.setVisibility(View.GONE);
progressLayout.setVisibility(View.VISIBLE);
} else { } else {
buttonLayout.setVisibility(View.VISIBLE); buttonLayout.setVisibility(View.VISIBLE);
progressLayout.setVisibility(View.GONE); progressLayout.setVisibility(View.GONE);
@ -1097,7 +1102,7 @@ public class AppDetailsRecyclerViewAdapter
private final View.OnClickListener onUpgradeClickListener = new View.OnClickListener() { private final View.OnClickListener onUpgradeClickListener = new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
callbacks.upgradeApk(); callbacks.installApk();
} }
}; };

View File

@ -56,7 +56,7 @@ import java.util.Iterator;
* </ul> * </ul>
* <p> * <p>
* The state of the UI is defined in a dumb {@link AppListItemState} class, then applied to the UI * The state of the UI is defined in a dumb {@link AppListItemState} class, then applied to the UI
* in the {@link #refreshView(App, AppUpdateStatus)} method. * in the {@link #updateAppStatus(App, AppUpdateStatus)} method.
*/ */
public abstract class AppListItemController extends RecyclerView.ViewHolder { public abstract class AppListItemController extends RecyclerView.ViewHolder {
@ -220,15 +220,10 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
} }
/** /**
* Updates both the progress bar and the circular install button (which shows progress around the outside of * Updates both the progress bar and the circular install button (which
* the circle). Also updates the app label to indicate that the app is being downloaded. * shows progress around the outside of the circle). Also updates the app
*/ * label to indicate that the app is being downloaded.
private void updateAppStatus(@NonNull App app, @Nullable AppUpdateStatus status) { * <p>
currentStatus = status;
refreshView(app, status);
}
/**
* Queries the current state via {@link #getCurrentViewState(App, AppUpdateStatus)} * Queries the current state via {@link #getCurrentViewState(App, AppUpdateStatus)}
* and then updates the relevant widgets depending on that state. * and then updates the relevant widgets depending on that state.
* <p> * <p>
@ -238,7 +233,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
* @see AppListItemState * @see AppListItemState
* @see #getCurrentViewState(App, AppUpdateStatus) * @see #getCurrentViewState(App, AppUpdateStatus)
*/ */
private void refreshView(@NonNull App app, @Nullable AppUpdateStatus appStatus) { private void updateAppStatus(@NonNull App app, @Nullable AppUpdateStatus appStatus) {
currentStatus = appStatus;
AppListItemState viewState = getCurrentViewState(app, appStatus); AppListItemState viewState = getCurrentViewState(app, appStatus);
@ -289,12 +285,14 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
if (viewState.shouldShowActionButton()) { if (viewState.shouldShowActionButton()) {
installButton.setVisibility(View.GONE); installButton.setVisibility(View.GONE);
} else if (viewState.showProgress()) { } else if (viewState.showProgress()) {
installButton.setEnabled(false);
installButton.setVisibility(View.VISIBLE); installButton.setVisibility(View.VISIBLE);
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress)); installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
int progressAsDegrees = viewState.getProgressMax() <= 0 ? 0 : int progressAsDegrees = viewState.getProgressMax() <= 0 ? 0 :
(int) (((float) viewState.getProgressCurrent() / viewState.getProgressMax()) * 360); (int) (((float) viewState.getProgressCurrent() / viewState.getProgressMax()) * 360);
installButton.setImageLevel(progressAsDegrees); installButton.setImageLevel(progressAsDegrees);
} else if (viewState.shouldShowInstall()) { } else if (viewState.shouldShowInstall()) {
installButton.setEnabled(true);
installButton.setVisibility(View.VISIBLE); installButton.setVisibility(View.VISIBLE);
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download)); installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download));
} else { } else {
@ -332,9 +330,13 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
case ReadyToInstall: case ReadyToInstall:
return getViewStateReadyToInstall(app); return getViewStateReadyToInstall(app);
case PendingInstall:
case Downloading: case Downloading:
return getViewStateDownloading(app, appStatus); return getViewStateDownloading(app, appStatus);
case Installing:
return getViewStateInstalling(app);
case Installed: case Installed:
return getViewStateInstalled(app); return getViewStateInstalled(app);
@ -344,6 +346,16 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
} }
} }
protected AppListItemState getViewStateInstalling(@NonNull App app) {
CharSequence mainText = activity.getString(
R.string.app_list__name__downloading_in_progress, app.name);
return new AppListItemState(app)
.setMainText(mainText)
.showActionButton(null)
.setStatusText(activity.getString(R.string.notification_content_single_installing, app.name));
}
protected AppListItemState getViewStateInstalled(@NonNull App app) { protected AppListItemState getViewStateInstalled(@NonNull App app) {
CharSequence mainText = activity.getString( CharSequence mainText = activity.getString(
R.string.app_list__name__successfully_installed, app.name); R.string.app_list__name__successfully_installed, app.name);

View File

@ -101,10 +101,6 @@ public class AppDetailsAdapterTest extends FDroidProviderTest {
} }
private final AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks dummyCallbacks = new AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks() { // NOCHECKSTYLE LineLength private final AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks dummyCallbacks = new AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks() { // NOCHECKSTYLE LineLength
@Override
public boolean isAppDownloading() {
return false;
}
@Override @Override
public void enableAndroidBeam() { public void enableAndroidBeam() {
@ -131,11 +127,6 @@ public class AppDetailsAdapterTest extends FDroidProviderTest {
} }
@Override
public void upgradeApk() {
}
@Override @Override
public void uninstallApk() { public void uninstallApk() {