diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1fc331021..2545fc625 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,13 +25,14 @@ package="com.aurora.store"> - + + + + + diff --git a/app/src/main/java/com/aurora/store/Constants.java b/app/src/main/java/com/aurora/store/Constants.java index 5c5314701..bc2cd6693 100644 --- a/app/src/main/java/com/aurora/store/Constants.java +++ b/app/src/main/java/com/aurora/store/Constants.java @@ -24,6 +24,8 @@ public class Constants { public static final String SHARED_PREFERENCES_KEY = "com.aurora.store"; public static final String SERVICE_PACKAGE = "com.aurora.services"; public static final String APP_DETAIL_URL = "https://play.google.com/store/apps/details?id="; + public static final String APP_ICON_URL = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png"; + public static final String UPDATE_URL = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json"; public static final String INTENT_DEVICE_NAME = "INTENT_DEVICE_NAME"; public static final String INTENT_DEVICE_INDEX = "INTENT_DEVICE_INDEX"; @@ -36,7 +38,6 @@ public class Constants { public static final String PUB_PREFIX = "pub:"; public static final String TAG = "Aurora Store"; public static final String FILES = "Files"; - public static final String GZIPPED = "GZipped"; public static final String RECENT_HISTORY = "RECENT_HISTORY"; @@ -108,4 +109,5 @@ public class Constants { public static final String PREFERENCE_TOP_FAMILY = "PREFERENCE_TOP_FAMILY"; public static final String PREFERENCE_INSTALLED_APPS = "PREFERENCE_INSTALLED_APPS"; public static final String PREFERENCE_CACHE_DATE = "PREFERENCE_CACHE_DATE"; + public static final String PREFERENCE_SELF_UPDATE_DATE = "PREFERENCE_SELF_UPDATE_DATE"; } diff --git a/app/src/main/java/com/aurora/store/SelfUpdateService.java b/app/src/main/java/com/aurora/store/SelfUpdateService.java new file mode 100644 index 000000000..b559ed0fa --- /dev/null +++ b/app/src/main/java/com/aurora/store/SelfUpdateService.java @@ -0,0 +1,205 @@ +package com.aurora.store; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.graphics.Color; +import android.os.Build; +import android.os.IBinder; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; + +import com.aurora.store.download.DownloadManager; +import com.aurora.store.download.RequestBuilder; +import com.aurora.store.model.App; +import com.aurora.store.model.Update; +import com.aurora.store.task.NetworkTask; +import com.aurora.store.utility.CertUtil; +import com.aurora.store.utility.Log; +import com.aurora.store.utility.PackageUtil; +import com.google.gson.Gson; +import com.tonyodev.fetch2.AbstractFetchGroupListener; +import com.tonyodev.fetch2.Download; +import com.tonyodev.fetch2.EnqueueAction; +import com.tonyodev.fetch2.Error; +import com.tonyodev.fetch2.Fetch; +import com.tonyodev.fetch2.FetchGroup; +import com.tonyodev.fetch2.FetchListener; +import com.tonyodev.fetch2.Request; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +public class SelfUpdateService extends Service { + + public static SelfUpdateService instance = null; + + private CompositeDisposable disposable = new CompositeDisposable(); + private int hashCode = BuildConfig.APPLICATION_ID.hashCode(); + private App app; + private Fetch fetch; + private FetchListener fetchListener; + private Gson gson = new Gson(); + + public static boolean isServiceRunning() { + try { + return instance != null && instance.isRunning(); + } catch (NullPointerException e) { + return false; + } + } + + private boolean isRunning() { + return true; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_NOT_STICKY; + } + + @Override + public void onCreate() { + super.onCreate(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + startForeground(1, getNotification()); + } else { + Notification notification = getNotification(new NotificationCompat.Builder(this)); + startForeground(1, notification); + } + startUpdate(); + } + + @Override + public void onDestroy() { + instance = null; + super.onDestroy(); + } + + private void destroyService() { + Log.e("Self-update service destroyed"); + stopForeground(true); + stopSelf(); + } + + private void startUpdate() { + disposable.add(Observable.fromCallable(() -> new NetworkTask(this) + .get(Constants.UPDATE_URL)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + try { + gson = new Gson(); + Update update = gson.fromJson(response, Update.class); + downloadAndUpdate(update); + } catch (Exception e) { + Log.e("Error checking self-update"); + destroyService(); + } + })); + } + + private void downloadAndUpdate(Update update) { + app = new App(); + app.setPackageName(BuildConfig.APPLICATION_ID); + app.setDisplayName(getString(R.string.app_name)); + app.setVersionName(update.getVersionName()); + app.setVersionCode(update.getVersionCode()); + + Request request = RequestBuilder.buildRequest(this, app, isFDroidVariant() ? update.getFdroidBuild() : update.getAuroraBuild()); + request.setEnqueueAction(EnqueueAction.REPLACE_EXISTING); + List requestList = new ArrayList<>(); + requestList.add(request); + + fetch = DownloadManager.getFetchInstance(this); + fetch.enqueue(requestList, result -> { + Log.d("Downloading latest self-update"); + }); + + fetchListener = getFetchListener(); + fetch.addListener(fetchListener); + + //Add and to PseudoMaps + PackageUtil.addToPseudoPackageMap(this, app.getPackageName(), app.getDisplayName()); + PackageUtil.addToPseudoURLMap(this, app.getPackageName(), Constants.APP_ICON_URL); + } + + private boolean isFDroidVariant() { + return CertUtil.isFDroidApp(this, BuildConfig.APPLICATION_ID); + } + + @RequiresApi(Build.VERSION_CODES.O) + private Notification getNotification() { + String NOTIFICATION_CHANNEL_ID = BuildConfig.APPLICATION_ID; + String channelName = "Self Update Service"; + + NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH); + notificationChannel.setLightColor(Color.BLUE); + notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + + NotificationManager manager = (NotificationManager) getSystemService(SelfUpdateService.NOTIFICATION_SERVICE); + manager.createNotificationChannel(notificationChannel); + + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); + return getNotification(notificationBuilder); + } + + private Notification getNotification(NotificationCompat.Builder builder) { + int versionCode = Build.VERSION.SDK_INT; + return builder + .setAutoCancel(true) + .setCategory(Notification.CATEGORY_PROGRESS) + .setContentTitle("Self update") + .setContentText("Updating Aurora Store in background") + .setOngoing(false) + .setPriority(versionCode >= Build.VERSION_CODES.O ? NotificationCompat.PRIORITY_DEFAULT : Notification.PRIORITY_DEFAULT) + .setSmallIcon(R.drawable.ic_update) + .build(); + } + + private FetchListener getFetchListener() { + return new AbstractFetchGroupListener() { + @Override + public void onError(int groupId, @NotNull Download download, @NotNull Error error, + @org.jetbrains.annotations.Nullable Throwable throwable, @NotNull FetchGroup fetchGroup) { + if (groupId == hashCode) { + Log.e("Error self-updating %s", app.getDisplayName()); + destroyService(); + } + } + + @Override + public void onCompleted(int groupId, @NotNull Download download, @NotNull FetchGroup fetchGroup) { + if (groupId == hashCode && fetchGroup.getGroupDownloadProgress() == 100) { + AuroraApplication.getInstaller().install(app); + destroyService(); + } + } + + @Override + public void onCancelled(int groupId, @NotNull Download download, @NotNull FetchGroup fetchGroup) { + if (groupId == hashCode) { + Log.e("Self-update cancelled %s", app.getDisplayName()); + destroyService(); + } + } + }; + } +} diff --git a/app/src/main/java/com/aurora/store/activity/AuroraActivity.java b/app/src/main/java/com/aurora/store/activity/AuroraActivity.java index fc4a85508..7ec713d0e 100644 --- a/app/src/main/java/com/aurora/store/activity/AuroraActivity.java +++ b/app/src/main/java/com/aurora/store/activity/AuroraActivity.java @@ -21,7 +21,6 @@ package com.aurora.store.activity; import android.Manifest; -import android.app.StatusBarManager; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; @@ -38,23 +37,37 @@ import androidx.core.graphics.ColorUtils; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import com.aurora.store.BuildConfig; import com.aurora.store.Constants; import com.aurora.store.R; +import com.aurora.store.SelfUpdateService; import com.aurora.store.adapter.ViewPagerAdapter; import com.aurora.store.fragment.AppsFragment; import com.aurora.store.fragment.HomeFragment; import com.aurora.store.fragment.SearchFragment; +import com.aurora.store.model.Update; +import com.aurora.store.task.NetworkTask; import com.aurora.store.utility.Accountant; +import com.aurora.store.utility.CertUtil; +import com.aurora.store.utility.Log; import com.aurora.store.utility.PrefUtil; +import com.aurora.store.utility.TextUtil; import com.aurora.store.utility.ThemeUtil; import com.aurora.store.utility.Util; import com.aurora.store.utility.ViewUtil; import com.aurora.store.view.CustomViewPager; import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.gson.Gson; + +import java.util.Calendar; import butterknife.BindView; import butterknife.ButterKnife; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; public class AuroraActivity extends AppCompatActivity { @@ -67,6 +80,7 @@ public class AuroraActivity extends AppCompatActivity { CustomViewPager viewPager; @BindView(R.id.bottom_navigation) BottomNavigationView bottomNavigationView; + private ActionBar actionBar; private ViewPagerAdapter pagerAdapter; private ThemeUtil themeUtil = new ThemeUtil(); @@ -86,6 +100,7 @@ public class AuroraActivity extends AppCompatActivity { ButterKnife.bind(this); fragmentCur = Util.getDefaultTab(this); onNewIntent(getIntent()); + if (!PrefUtil.getBoolean(this, Constants.PREFERENCE_DO_NOT_SHOW_INTRO)) { PrefUtil.putBoolean(this, Constants.PREFERENCE_DO_NOT_SHOW_INTRO, true); startActivity(new Intent(this, IntroActivity.class)); @@ -100,6 +115,9 @@ public class AuroraActivity extends AppCompatActivity { if (Util.isCacheObsolete(this)) Util.clearCache(this); + if (Util.shouldCheckUpdate(this)) + checkSelfUpdate(); + checkPermissions(); } @@ -187,7 +205,6 @@ public class AuroraActivity extends AppCompatActivity { disposable.clear(); } - private void setupActionbar() { setSupportActionBar(toolbar); actionBar = getSupportActionBar(); @@ -240,6 +257,35 @@ public class AuroraActivity extends AppCompatActivity { .getItem(fragmentCur).getItemId()); } + private void checkSelfUpdate() { + disposable.add(Observable.fromCallable(() -> new NetworkTask(this) + .get(Constants.UPDATE_URL)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + try { + Util.setSelfUpdateTime(this, Calendar.getInstance().getTimeInMillis()); + Gson gson = new Gson(); + Update update = gson.fromJson(response, Update.class); + if (update.getVersionCode() > BuildConfig.VERSION_CODE) { + if (CertUtil.isFDroidApp(this, BuildConfig.APPLICATION_ID) + && TextUtil.emptyIfNull(update.getFdroidBuild()).isEmpty()) { + Log.d("FDroid build of latest version is not published yet"); + return; + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + showAddRepoDialog(update); + } else { + Log.i("No new update available"); + } + } + } catch (Exception e) { + Log.e("Error checking updates"); + } + })); + } + private void checkPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { @@ -248,4 +294,25 @@ public class AuroraActivity extends AppCompatActivity { 1337); } } + + protected void showAddRepoDialog(Update update) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this) + .setTitle("New update available") + .setMessage(new StringBuilder() + .append(update.getVersionName()) + .append("\nChangelog:\n") + .append(TextUtil.emptyIfNull(update.getChangelog())) + .append("\n") + .append("Do you wish to update now ?") + .toString()) + .setPositiveButton(getString(android.R.string.yes), (dialog, which) -> { + Intent intent = new Intent(this, SelfUpdateService.class); + startService(intent); + }) + .setNegativeButton(getString(android.R.string.no), (dialog, which) -> { + dialog.dismiss(); + }); + builder.create(); + builder.show(); + } } diff --git a/app/src/main/java/com/aurora/store/model/Update.java b/app/src/main/java/com/aurora/store/model/Update.java new file mode 100644 index 000000000..fd1078bd5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/model/Update.java @@ -0,0 +1,62 @@ +package com.aurora.store.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Update { + @SerializedName("version_name") + @Expose + private String versionName; + @SerializedName("version_code") + @Expose + private Integer versionCode; + @SerializedName("aurora_build") + @Expose + private String auroraBuild; + @SerializedName("fdroid_build") + @Expose + private String fdroidBuild; + @SerializedName("changelog") + @Expose + private String changelog; + + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + } + + public Integer getVersionCode() { + return versionCode; + } + + public void setVersionCode(Integer versionCode) { + this.versionCode = versionCode; + } + + public String getAuroraBuild() { + return auroraBuild; + } + + public void setAuroraBuild(String auroraBuild) { + this.auroraBuild = auroraBuild; + } + + public String getFdroidBuild() { + return fdroidBuild; + } + + public void setFdroidBuild(String fdroidBuild) { + this.fdroidBuild = fdroidBuild; + } + + public String getChangelog() { + return changelog; + } + + public void setChangelog(String changelog) { + this.changelog = changelog; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/utility/Util.java b/app/src/main/java/com/aurora/store/utility/Util.java index 9a2064ee5..e11a6003c 100644 --- a/app/src/main/java/com/aurora/store/utility/Util.java +++ b/app/src/main/java/com/aurora/store/utility/Util.java @@ -489,7 +489,7 @@ public class Util { Log.i("Periodic update preferences updated"); } - public static void clearOldInstallationSessions(Context context){ + public static void clearOldInstallationSessions(Context context) { final PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); for (PackageInstaller.SessionInfo sessionInfo : packageInstaller.getMySessions()) { final int sessionId = sessionInfo.getSessionId(); @@ -501,4 +501,20 @@ public class Util { } } } + + public static boolean shouldCheckUpdate(Context context) { + try { + long lastSyncDate = Long.parseLong(PrefUtil.getString(context, Constants.PREFERENCE_SELF_UPDATE_DATE)); + long currentSyncDate = Calendar.getInstance().getTimeInMillis(); + long diffDatesInMillis = currentSyncDate - lastSyncDate; + long diffInDays = TimeUnit.MILLISECONDS.toDays(diffDatesInMillis); + return diffInDays >= 1; + } catch (Exception e) { + return true; + } + } + + public static void setSelfUpdateTime(Context context, Long dateInMillis) { + PrefUtil.putString(context, Constants.PREFERENCE_SELF_UPDATE_DATE, String.valueOf(dateInMillis)); + } }