diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java index 6cb8079e10..90aa1462c1 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java @@ -35,11 +35,13 @@ import com.github.adamantcheese.chan.core.manager.BoardManager; import com.github.adamantcheese.chan.core.manager.FilterWatchManager; import com.github.adamantcheese.chan.core.manager.ReportManager; +import com.github.adamantcheese.chan.core.manager.SettingsNotificationManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.SiteService; import com.github.adamantcheese.chan.ui.service.LastPageNotification; import com.github.adamantcheese.chan.ui.service.SavingNotification; import com.github.adamantcheese.chan.ui.service.WatchNotification; +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; @@ -76,6 +78,9 @@ public class Chan @Inject ReportManager reportManager; + @Inject + SettingsNotificationManager settingsNotificationManager; + private static Feather feather; public static T instance(Class tClass) { @@ -200,8 +205,10 @@ public void onCreate() { System.exit(999); }); - if (ChanSettings.autoCrashLogsUpload.get()) { - reportManager.sendCollectedCrashLogs(); + if (ChanSettings.collectCrashLogs.get()) { + if (reportManager.hasCrashLogs()) { + settingsNotificationManager.notify(SettingNotificationType.CrashLog); + } } } @@ -238,7 +245,7 @@ private void onUnhandledException(Throwable exception, String error) { return; } - if (ChanSettings.autoCrashLogsUpload.get()) { + if (ChanSettings.collectCrashLogs.get()) { reportManager.storeCrashLog(exception.getMessage(), error); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/controller/Controller.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/controller/Controller.java index fb63d0cacd..a5e4919d49 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/controller/Controller.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/controller/Controller.java @@ -21,6 +21,8 @@ import android.view.KeyEvent; import android.view.ViewGroup; +import androidx.annotation.CallSuper; + import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.controller.transition.FadeInTransition; import com.github.adamantcheese.chan.controller.transition.FadeOutTransition; @@ -73,6 +75,7 @@ public Controller(Context context) { this.context = context; } + @CallSuper public void onCreate() { alive = true; if (LOG_STATES) { @@ -80,6 +83,7 @@ public void onCreate() { } } + @CallSuper public void onShow() { shown = true; if (LOG_STATES) { @@ -95,6 +99,7 @@ public void onShow() { } } + @CallSuper public void onHide() { shown = false; if (LOG_STATES) { @@ -110,6 +115,7 @@ public void onHide() { } } + @CallSuper public void onDestroy() { alive = false; if (LOG_STATES) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java index 383994ad56..a4ec2d3c7d 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java @@ -28,6 +28,7 @@ import com.github.adamantcheese.chan.core.manager.ReplyManager; import com.github.adamantcheese.chan.core.manager.ReportManager; import com.github.adamantcheese.chan.core.manager.SavedThreadLoaderManager; +import com.github.adamantcheese.chan.core.manager.SettingsNotificationManager; import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.manager.WakeManager; import com.github.adamantcheese.chan.core.manager.WatchManager; @@ -175,7 +176,8 @@ public MockReplyManager provideMockReplyManager() { public ReportManager provideReportManager( NetModule.ProxiedOkHttpClient okHttpClient, Gson gson, - ThreadSaveManager threadSaveManager + ThreadSaveManager threadSaveManager, + SettingsNotificationManager settingsNotificationManager ) { Logger.d(AppModule.DI_TAG, "Report manager"); File cacheDir = getCacheDir(); @@ -183,8 +185,15 @@ public ReportManager provideReportManager( return new ReportManager( okHttpClient.getProxiedClient(), threadSaveManager, + settingsNotificationManager, gson, new File(cacheDir, CRASH_LOGS_DIR_NAME) ); } + + @Provides + @Singleton + public SettingsNotificationManager provideSettingsNotificationManager() { + return new SettingsNotificationManager(); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ReportManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ReportManager.kt index b004a5b38f..8fc0b9ac5a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ReportManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ReportManager.kt @@ -6,6 +6,8 @@ import com.github.adamantcheese.chan.BuildConfig import com.github.adamantcheese.chan.core.base.ModularResult import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.adamantcheese.chan.ui.controller.LogsController +import com.github.adamantcheese.chan.ui.layout.crashlogs.CrashLog +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType import com.github.adamantcheese.chan.utils.BackgroundUtils import com.github.adamantcheese.chan.utils.Logger import com.github.adamantcheese.chan.utils.TimeUtils.getCurrentDateAndTimeUTC @@ -29,6 +31,7 @@ import java.util.concurrent.atomic.AtomicInteger class ReportManager( private val okHttpClient: OkHttpClient, private val threadSaveManager: ThreadSaveManager, + private val settingsNotificationManager: SettingsNotificationManager, private val gson: Gson, private val crashLogsDirPath: File ) { @@ -74,6 +77,13 @@ class ReportManager( return@flatMapSingle processSingleRequest(request, crashLogFile) } } + .debounce(1, TimeUnit.SECONDS) + .doOnNext { + // If no more crash logs left, remove the notification + if (!hasCrashLogs()) { + settingsNotificationManager.cancel(SettingNotificationType.CrashLog) + } + } .subscribe({ // Do nothing }, { error -> @@ -126,7 +136,7 @@ class ReportManager( return } - val time = System.nanoTime() + val time = System.currentTimeMillis() val newCrashLog = File(crashLogsDirPath, "${CRASH_LOG_FILE_NAME_PREFIX}_${time}.txt") if (newCrashLog.exists()) { @@ -169,37 +179,96 @@ class ReportManager( Logger.d(TAG, "Stored new crash log, path = ${newCrashLog.absolutePath}") } - // Since this is a singleton we don't care about disposing of this thing because nothing may - // leak here - @SuppressLint("CheckResult") - fun sendCollectedCrashLogs() { + fun hasCrashLogs(): Boolean { + if (!createCrashLogsDirIfNotExists()) { + return false + } + + val crashLogs = crashLogsDirPath.listFiles() + return crashLogs != null && crashLogs.isNotEmpty() + } + + fun countCrashLogs(): Int { + if (!createCrashLogsDirIfNotExists()) { + return 0 + } + + return crashLogsDirPath.listFiles()?.size ?: 0 + } + + fun getCrashLogs(): List { if (!createCrashLogsDirIfNotExists()) { + return emptyList() + } + + return crashLogsDirPath.listFiles() + ?.sortedByDescending { file -> file.lastModified() } + ?.toList() ?: emptyList() + } + + fun deleteCrashLogs(crashLogs: List) { + if (!createCrashLogsDirIfNotExists()) { + settingsNotificationManager.cancel(SettingNotificationType.CrashLog) + return + } + + crashLogs.forEach { crashLog -> crashLog.file.delete() } + + val remainingCrashLogs = crashLogsDirPath.listFiles()?.size ?: 0 + if (remainingCrashLogs == 0) { + settingsNotificationManager.cancel(SettingNotificationType.CrashLog) return } + // There are still crash logs left, so show the notifications if they are not shown yet + settingsNotificationManager.notify(SettingNotificationType.CrashLog) + } + + fun deleteAllCrashLogs() { + if (!createCrashLogsDirIfNotExists()) { + settingsNotificationManager.cancel(SettingNotificationType.CrashLog) + return + } + + val potentialCrashLogs = crashLogsDirPath.listFiles() + if (potentialCrashLogs.isNullOrEmpty()) { + Logger.d(TAG, "No new crash logs") + settingsNotificationManager.cancel(SettingNotificationType.CrashLog) + return + } + + potentialCrashLogs.asSequence() + .forEach { crashLogFile -> crashLogFile.delete() } + + val remainingCrashLogs = crashLogsDirPath.listFiles()?.size ?: 0 + if (remainingCrashLogs == 0) { + settingsNotificationManager.cancel(SettingNotificationType.CrashLog) + return + } + + // There are still crash logs left, so show the notifications if they are not shown yet + settingsNotificationManager.notify(SettingNotificationType.CrashLog) + } + + fun sendCrashLogs(crashLogs: List): Completable { + if (!createCrashLogsDirIfNotExists()) { + return Completable.complete() + } + + if (crashLogs.isEmpty()) { + return Completable.complete() + } + // Collect and create reports on a background thread because logs may wait quite a lot now // and it may lag the UI. - Completable.fromAction { + return Completable.fromAction { BackgroundUtils.ensureBackgroundThread() - val potentialCrashLogs = crashLogsDirPath.listFiles() - if (potentialCrashLogs.isNullOrEmpty()) { - Logger.d(TAG, "No new crash logs") - return@fromAction - } - - potentialCrashLogs.asSequence() - .filter { file -> file.name.startsWith(CRASH_LOG_FILE_NAME_PREFIX) } - .map { file -> createReportRequest(file) } - .filterNotNull() + crashLogs + .mapNotNull { crashLog -> createReportRequest(crashLog) } .forEach { request -> crashLogSenderQueue.onNext(request) } } - .subscribeOn(senderScheduler) - .subscribe({ - // Do nothing - }, { error -> - Logger.e(TAG, "Error while collecting logs: ${error}") - }) + .subscribeOn(senderScheduler) } fun sendReport(title: String, description: String, logs: String?): Single> { @@ -261,11 +330,11 @@ class ReportManager( "active: $filesLocationActiveDirType" } - private fun createReportRequest(file: File): ReportRequestWithFile? { + private fun createReportRequest(crashLog: CrashLog): ReportRequestWithFile? { BackgroundUtils.ensureBackgroundThread() val log = try { - file.readText() + crashLog.file.readText() } catch (error: Throwable) { Logger.e(TAG, "Error reading crash log file", error) return null @@ -282,7 +351,7 @@ class ReportManager( return ReportRequestWithFile( reportRequest = request, - crashLogFile = file + crashLogFile = crashLog.file ) } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SettingsNotificationManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SettingsNotificationManager.kt new file mode 100644 index 0000000000..10594b18d1 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SettingsNotificationManager.kt @@ -0,0 +1,97 @@ +package com.github.adamantcheese.chan.core.manager + +import androidx.annotation.GuardedBy +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType +import com.github.adamantcheese.chan.utils.Logger +import io.reactivex.Flowable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.processors.BehaviorProcessor + +class SettingsNotificationManager { + @GuardedBy("this") + private val notifications: MutableSet = mutableSetOf() + + /** + * A reactive stream that is being used to notify observers about [notifications] changes + * */ + private val activeNotificationsSubject = BehaviorProcessor.createDefault(Unit) + + /** + * If [notifications] doesn't contain [notificationType] yet, then notifies + * all observers that there is a new notification + * */ + @Synchronized + fun notify(notificationType: SettingNotificationType) { + if (notifications.add(notificationType)) { + Logger.d(TAG, "Added ${notificationType.name} notification") + activeNotificationsSubject.onNext(Unit) + } + } + + @Synchronized + fun getNotificationByPriority(): SettingNotificationType? { + if (contains(SettingNotificationType.ApkUpdate)) { + return SettingNotificationType.ApkUpdate + } + + if (contains(SettingNotificationType.CrashLog)) { + return SettingNotificationType.CrashLog + } + + // Add new notifications here. Don't forget that order matters! The order affects priority. + // For now "Apk update" has higher priority than "Crash log". + + return null + } + + @Synchronized + fun hasNotifications(notificationType: SettingNotificationType): Boolean { + return contains(notificationType) + } + + @Synchronized + fun notificationsCount(): Int = notifications.count() + + @Synchronized + fun getOrDefault(notificationType: SettingNotificationType): SettingNotificationType { + if (!contains(notificationType)) { + return SettingNotificationType.Default + } + + return notificationType + } + + @Synchronized + fun count(): Int = notifications.size + + @Synchronized + fun contains(notificationType: SettingNotificationType): Boolean { + return notifications.contains(notificationType) + } + + /** + * If [notifications] contains [notificationType], then notifies all observers that this + * notification has been canceled + * */ + @Synchronized + fun cancel(notificationType: SettingNotificationType) { + if (notifications.remove(notificationType)) { + Logger.d(TAG, "Removed ${notificationType.name} notification") + activeNotificationsSubject.onNext(Unit) + } + } + + /** + * Use this to observe current notification state. Duplicates checks and everything else is done + * internally so you don't have to worry that you will get the same state twice. All updates + * come on main thread so there is no need to worry about that as well. + * */ + fun listenForNotificationUpdates(): Flowable = activeNotificationsSubject + .onBackpressureBuffer() + .observeOn(AndroidSchedulers.mainThread()) + .hide() + + companion object { + private const val TAG = "SettingsNotificationManager" + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java index d1e716c994..7aabf72a5b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java @@ -44,7 +44,9 @@ import com.github.adamantcheese.chan.core.net.UpdateApiRequest; import com.github.adamantcheese.chan.core.net.UpdateApiRequest.UpdateApiResponse; import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.core.settings.state.PersistableChanState; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType; import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.Logger; import com.github.k1rakishou.fsaf.FileChooser; @@ -93,6 +95,9 @@ public class UpdateManager { @Inject FileManager fileManager; + @Inject + SettingsNotificationManager settingsNotificationManager; + @Inject FileChooser fileChooser; @@ -112,6 +117,7 @@ public UpdateManager(Context context) { */ public void autoUpdateCheck() { if (ChanSettings.previousVersion.get() < BuildConfig.VERSION_CODE && ChanSettings.previousVersion.get() != 0) { + // Show dialog because release updates are infrequent so it's fine Spanned text = Html.fromHtml( "

" + getApplicationLabel() + " was updated to " + BuildConfig.VERSION_NAME + "

"); final AlertDialog dialog = @@ -128,19 +134,19 @@ public void autoUpdateCheck() { // Also set the new app version to not show this message again ChanSettings.previousVersion.set(BuildConfig.VERSION_CODE); + cancelApkUpdateNotification(); // Don't process the updater because a dialog is now already showing. return; } if (BuildConfig.DEV_BUILD && !ChanSettings.previousDevHash.get().equals(BuildConfig.COMMIT_HASH)) { - final AlertDialog dialog = new AlertDialog.Builder(context).setMessage( - getApplicationLabel() + " was updated to the latest commit.") - .setPositiveButton(R.string.ok, null) - .create(); - dialog.setCanceledOnTouchOutside(true); - dialog.show(); + // Show toast because dev updates may happen every day (to avoid alert dialog spam) + showToast(context, getApplicationLabel() + " was updated to the latest commit."); + ChanSettings.previousDevHash.set(BuildConfig.COMMIT_HASH); + cancelApkUpdateNotification(); + return; } @@ -152,6 +158,12 @@ public void manualUpdateCheck() { } private void runUpdateApi(final boolean manual) { + if (PersistableChanState.getHasNewApkUpdate()) { + // If we noticed that there was an apk update on the previous check - show the + // notification + notifyNewApkUpdate(); + } + if (!manual) { long lastUpdateTime = ChanSettings.updateCheckTime.get(); long interval = DAYS.toMillis(BuildConfig.UPDATE_DELAY); @@ -168,7 +180,7 @@ private void runUpdateApi(final boolean manual) { if (!BuildConfig.DEV_BUILD) { //region Release build volleyRequestQueue.add(new UpdateApiRequest(response -> { - if (!processUpdateApiResponse(response) && manual && BackgroundUtils.isInForeground()) { + if (!processUpdateApiResponse(response, manual) && manual && BackgroundUtils.isInForeground()) { new AlertDialog.Builder(context).setTitle(getString(R.string.update_none, getApplicationLabel())) .setPositiveButton(R.string.ok, null) .show(); @@ -195,6 +207,8 @@ private void runUpdateApi(final boolean manual) { .setPositiveButton(R.string.ok, null) .show(); } + + cancelApkUpdateNotification(); } else { //new version or commit, update Matcher versionCodeStringMatcher = Pattern.compile("(\\d+)(\\d{2})(\\d{2})") @@ -210,7 +224,7 @@ private void runUpdateApi(final boolean manual) { fauxResponse.apkURL = HttpUrl.parse(BuildConfig.DEV_API_ENDPOINT + "/apk/" + versionCode + "_" + commitHash + ".apk"); fauxResponse.body = SpannableStringBuilder.valueOf("New dev build; see commits!"); - processUpdateApiResponse(fauxResponse); + processUpdateApiResponse(fauxResponse, manual); } else { throw new Exception(); // to reuse the failed code below } @@ -227,25 +241,49 @@ private void runUpdateApi(final boolean manual) { } } - private boolean processUpdateApiResponse(UpdateApiResponse response) { + private boolean processUpdateApiResponse(UpdateApiResponse response, boolean manual) { if ((response.versionCode > BuildConfig.VERSION_CODE || BuildConfig.DEV_BUILD) && BackgroundUtils.isInForeground()) { - boolean concat = !response.updateTitle.isEmpty(); - CharSequence updateMessage = - concat ? TextUtils.concat(response.updateTitle, "; ", response.body) : response.body; - AlertDialog dialog = new AlertDialog.Builder(context).setTitle( - getApplicationLabel() + " " + response.versionCodeString + " available") - .setMessage(updateMessage) - .setNegativeButton(R.string.update_later, null) - .setPositiveButton(R.string.update_install, (dialog1, which) -> updateInstallRequested(response)) - .create(); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); + + // Do not spam dialogs if this is not the manual update check, use the notifications + // instead + if (manual) { + boolean concat = !response.updateTitle.isEmpty(); + CharSequence updateMessage = + concat ? TextUtils.concat(response.updateTitle, "; ", response.body) : response.body; + AlertDialog dialog = new AlertDialog.Builder(context).setTitle( + getApplicationLabel() + " " + response.versionCodeString + " available") + .setMessage(updateMessage) + .setNegativeButton(R.string.update_later, null) + .setPositiveButton(R.string.update_install, (dialog1, which) -> updateInstallRequested(response)) + .create(); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + + // There is an update, show the notification. + // + // (In case of the dev build we check whether the apk hashes differ or not beforehand, + // so if they are the same this method won't even get called. In case of the release + // build this method will be called in both cases so we do the check in this method) + notifyNewApkUpdate(); return true; } + + cancelApkUpdateNotification(); return false; } + private void notifyNewApkUpdate() { + PersistableChanState.setHasNewApkUpdate(true); + settingsNotificationManager.notify(SettingNotificationType.ApkUpdate); + } + + private void cancelApkUpdateNotification() { + PersistableChanState.setHasNewApkUpdate(false); + settingsNotificationManager.cancel(SettingNotificationType.ApkUpdate); + } + private void failedUpdate(boolean manual) { Logger.e(TAG, "Failed to process " + (BuildConfig.DEV_BUILD ? "dev" : "stable") + " API call for updating"); if (manual && BackgroundUtils.isInForeground()) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java index 820dae275b..99c3f4cdc3 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java @@ -914,7 +914,7 @@ public Object onPopulatePostOptions(Post post, List menu, List if (!loadable.isLocal()) { boolean isSaved = databaseManager.getDatabaseSavedReplyManager().isSaved(post.board, post.no); - extraMenu.add(new FloatingMenuItem(POST_OPTION_SAVE, isSaved ? R.string.unsave : R.string.save)); + extraMenu.add(new FloatingMenuItem(POST_OPTION_SAVE, isSaved ? R.string.unmark_as_my_post : R.string.mark_as_my_post)); if (BuildConfig.DEV_BUILD && loadable.no > 0) { extraMenu.add(new FloatingMenuItem(POST_OPTION_MOCK_REPLY, R.string.mock_reply)); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/BooleanSetting.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/BooleanSetting.java index 80603d9911..6102290f14 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/BooleanSetting.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/BooleanSetting.java @@ -48,4 +48,12 @@ public void set(Boolean value) { public void toggle() { set(!get()); } + + public void setSync(Boolean value) { + if (!value.equals(get())) { + settingProvider.putBooleanSync(key, value); + cached = value; + onValueChanged(); + } + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index ce804c163e..e49af92ff9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -48,6 +48,7 @@ public class ChanSettings { public static final String EMPTY_JSON = "{}"; public static final String NOTIFY_ALL_POSTS = "all"; public static final String NOTIFY_ONLY_QUOTES = "quotes"; + public static final String NO_HASH_SET = "NO_HASH_SET"; public enum MediaAutoLoadMode implements OptionSettingItem { @@ -266,7 +267,7 @@ public String getKey() { public static final OptionsSetting imageClickPreloadStrategy; public static final BooleanSetting imageViewerGestures; public static final BooleanSetting allowFilePickChooser; - public static final BooleanSetting autoCrashLogsUpload; + public static final BooleanSetting collectCrashLogs; public static final BooleanSetting captchaOnBottom; public static final BooleanSetting showCopyApkUpdateDialog; public static final BooleanSetting crashOnSafeThrow; @@ -394,7 +395,7 @@ public String getKey() { parsePostImageLinks = new BooleanSetting(p, "parse_post_image_links", true); - previousDevHash = new StringSetting(p, "previous_dev_hash", "NO_HASH_SET"); + previousDevHash = new StringSetting(p, "previous_dev_hash", NO_HASH_SET); addDubs = new BooleanSetting(p, "add_dubs", false); transparencyOn = new BooleanSetting(p, "image_transparency_on", false); youtubeTitleCache = new StringSetting(p, "yt_title_cache", EMPTY_JSON); @@ -413,7 +414,9 @@ public String getKey() { ); imageViewerGestures = new BooleanSetting(p, "image_viewer_gestures", true); allowFilePickChooser = new BooleanSetting(p, "allow_file_picker_chooser", false); - autoCrashLogsUpload = new BooleanSetting(p, "auto_upload_crash_logs", true); + // "auto_upload_crash_logs" is the old name of this setting. To avoid compatibility + // issues it was decided to leave it's name as is. + collectCrashLogs = new BooleanSetting(p, "auto_upload_crash_logs", true); captchaOnBottom = new BooleanSetting(p, "captcha_on_bottom", true); showCopyApkUpdateDialog = new BooleanSetting(p, "show_copy_apk_update_dialog", true); crashOnSafeThrow = new BooleanSetting(p, "crash_on_safe_throw", true); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/LongSetting.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/LongSetting.java index ffd3ed194d..15522e45c4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/LongSetting.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/LongSetting.java @@ -44,4 +44,12 @@ public void set(Long value) { onValueChanged(); } } + + public void setSync(Long value) { + if (!value.equals(get())) { + settingProvider.putLongSync(key, value); + cached = value; + onValueChanged(); + } + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java index d7a5c5b48b..7ee8c19160 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java @@ -38,4 +38,8 @@ public interface SettingProvider { void removeSync(String key); void putIntSync(String key, Integer value); + + void putLongSync(String key, Long value); + + void putBooleanSync(String key, Boolean value); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java index 8f47c926ea..1320715086 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java @@ -92,4 +92,14 @@ public void removeSync(String key) { public void putIntSync(String key, Integer value) { prefs.edit().putInt(key, value).commit(); } + + @Override + public void putLongSync(String key, Long value) { + prefs.edit().putLong(key, value).commit(); + } + + @Override + public void putBooleanSync(String key, Boolean value) { + prefs.edit().putBoolean(key, value).commit(); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java index 265f36ecad..152d8cc9a0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java @@ -138,4 +138,14 @@ public void removeSync(String key) { public void putIntSync(String key, Integer value) { throw new UnsupportedOperationException(); } + + @Override + public void putLongSync(String key, Long value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putBooleanSync(String key, Boolean value) { + throw new UnsupportedOperationException(); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/state/PersistableChanState.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/state/PersistableChanState.kt new file mode 100644 index 0000000000..889322f4e8 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/state/PersistableChanState.kt @@ -0,0 +1,32 @@ +package com.github.adamantcheese.chan.core.settings.state + +import com.github.adamantcheese.chan.core.settings.BooleanSetting +import com.github.adamantcheese.chan.core.settings.SharedPreferencesSettingProvider +import com.github.adamantcheese.chan.utils.AndroidUtils +import com.github.adamantcheese.chan.utils.Logger + + +object PersistableChanState { + private const val TAG = "ChanState" + private val hasNewApkUpdate: BooleanSetting + + init { + try { + val p = SharedPreferencesSettingProvider(AndroidUtils.getAppState()) + hasNewApkUpdate = BooleanSetting(p, "has_new_apk_update", false) + } catch (error: Throwable) { + Logger.e(TAG, "Error while initializing the state", error) + throw error + } + } + + // Why? So it can be mocked in tests. + @JvmStatic + fun getHasNewApkUpdate(): Boolean = hasNewApkUpdate.get() + + @JvmStatic + fun setHasNewApkUpdate(value: Boolean) = hasNewApkUpdate.set(value) + + @JvmStatic + fun setHasNewApkUpdateSync(value: Boolean) = hasNewApkUpdate.setSync(value) +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/site/sites/chan4/Chan4BoardsRequest.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/site/sites/chan4/Chan4BoardsRequest.java index 9b6c2b107d..27245cdf08 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/site/sites/chan4/Chan4BoardsRequest.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/site/sites/chan4/Chan4BoardsRequest.java @@ -26,7 +26,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class Chan4BoardsRequest diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/adapter/DrawerAdapter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/adapter/DrawerAdapter.java index 7ac6f91fa4..890086d9a8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/adapter/DrawerAdapter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/adapter/DrawerAdapter.java @@ -30,12 +30,14 @@ import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.core.manager.SettingsNotificationManager; import com.github.adamantcheese.chan.core.manager.WatchManager; import com.github.adamantcheese.chan.core.model.orm.Board; import com.github.adamantcheese.chan.core.model.orm.Pin; @@ -44,9 +46,12 @@ import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.PinHelper; import com.github.adamantcheese.chan.ui.helper.PostHelper; +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType; import com.github.adamantcheese.chan.ui.theme.ThemeHelper; import com.github.adamantcheese.chan.ui.view.ThumbnailView; import com.github.adamantcheese.chan.utils.AnimationUtils; +import com.github.adamantcheese.chan.utils.BackgroundUtils; +import com.github.adamantcheese.chan.utils.Logger; import com.github.adamantcheese.chan.utils.StringUtils; import javax.inject.Inject; @@ -61,9 +66,14 @@ import static com.github.adamantcheese.chan.utils.AndroidUtils.inflate; import static com.github.adamantcheese.chan.utils.AndroidUtils.setRoundItemBackground; import static com.github.adamantcheese.chan.utils.AndroidUtils.sp; +import static com.github.adamantcheese.chan.utils.AndroidUtils.updatePaddings; public class DrawerAdapter extends RecyclerView.Adapter { + private static final String TAG = "DrawerAdapter"; + + private static final int SETTINGS_OFFSET = 0; + //PIN_OFFSET is the number of items before the pins //(in this case, settings, history, and the bookmarked threads title) private static final int PIN_OFFSET = 3; @@ -78,6 +88,8 @@ public class DrawerAdapter @Inject WatchManager watchManager; + @Inject + SettingsNotificationManager settingsNotificationManager; private Context context; private Drawable downloadIconOutline; @@ -87,6 +99,7 @@ public class DrawerAdapter private Pin highlighted; private Bitmap archivedIcon; + public DrawerAdapter(Callback callback, Context context) { inject(this); this.callback = callback; @@ -179,6 +192,8 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (position) { case 0: linkHolder.text.setText(R.string.drawer_settings); + updateNotificationIcon(linkHolder); + ThemeHelper.getTheme().settingsDrawable.apply(linkHolder.image); break; case 1: @@ -195,6 +210,38 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { } } + private void updateNotificationIcon(LinkHolder linkHolder) { + SettingNotificationType notificationType + = settingsNotificationManager.getNotificationByPriority(); + + String notificationTypeString = "null"; + if (notificationType != null) { + notificationTypeString = notificationType.name(); + } + + Logger.d(TAG, "updateNotificationIcon() called notificationType = " + notificationTypeString); + + if (notificationType != null) { + int color = context.getResources().getColor( + notificationType.getNotificationIconTintColor() + ); + + linkHolder.notificationIcon.setVisibility(VISIBLE); + linkHolder.notificationIcon.setColorFilter(color); + + int totalNotificationsCount = settingsNotificationManager.notificationsCount(); + if (totalNotificationsCount > 1) { + linkHolder.totalNotificationsCount.setVisibility(VISIBLE); + linkHolder.totalNotificationsCount.setText(String.valueOf(totalNotificationsCount)); + } else { + linkHolder.totalNotificationsCount.setVisibility(GONE); + } + } else { + linkHolder.notificationIcon.setVisibility(GONE); + linkHolder.totalNotificationsCount.setVisibility(GONE); + } + } + @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); @@ -237,6 +284,13 @@ public int getItemViewType(int position) { } } + public void onNotificationsChanged() { + Logger.d(TAG, "onNotificationsChanged called"); + + BackgroundUtils.ensureMainThread(); + notifyItemChanged(SETTINGS_OFFSET); + } + public void onPinAdded(Pin pin) { notifyItemInserted(watchManager.getAllPins().indexOf(pin) + PIN_OFFSET); } @@ -504,12 +558,17 @@ private class LinkHolder extends RecyclerView.ViewHolder { private ImageView image; private TextView text; + private AppCompatImageView notificationIcon; + private AppCompatTextView totalNotificationsCount; private LinkHolder(View itemView) { super(itemView); image = itemView.findViewById(R.id.image); text = itemView.findViewById(R.id.text); text.setTypeface(ThemeHelper.getTheme().mainFont); + notificationIcon = itemView.findViewById(R.id.setting_notification_icon); + totalNotificationsCount = itemView.findViewById(R.id.setting_notification_total_count); + updatePaddings(notificationIcon, dp(4), dp(4), dp(4), dp(4)); itemView.setOnClickListener(v -> { switch (getAdapterPosition()) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/DrawerController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/DrawerController.java index f87e4fed45..67dbc73645 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/DrawerController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/DrawerController.java @@ -32,6 +32,7 @@ import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.controller.Controller; import com.github.adamantcheese.chan.controller.NavigationController; +import com.github.adamantcheese.chan.core.manager.SettingsNotificationManager; import com.github.adamantcheese.chan.core.manager.WatchManager; import com.github.adamantcheese.chan.core.manager.WatchManager.PinMessages; import com.github.adamantcheese.chan.core.model.orm.Loadable; @@ -41,6 +42,7 @@ import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.adapter.DrawerAdapter; import com.github.adamantcheese.chan.ui.controller.settings.MainSettingsController; +import com.github.adamantcheese.chan.utils.Logger; import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; @@ -50,6 +52,9 @@ import javax.inject.Inject; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.core.model.orm.Loadable.LoadableDownloadingState.DownloadingAndNotViewable; import static com.github.adamantcheese.chan.core.model.orm.Loadable.LoadableDownloadingState.DownloadingAndViewable; @@ -64,14 +69,19 @@ public class DrawerController extends Controller implements DrawerAdapter.Callback, View.OnClickListener { + private static final String TAG = "DrawerController"; + protected FrameLayout container; protected DrawerLayout drawerLayout; protected LinearLayout drawer; protected RecyclerView recyclerView; protected DrawerAdapter drawerAdapter; + private CompositeDisposable compositeDisposable = new CompositeDisposable(); @Inject WatchManager watchManager; + @Inject + SettingsNotificationManager settingsNotificationManager; public DrawerController(Context context) { super(context); @@ -101,12 +111,22 @@ public void onCreate() { itemTouchHelper.attachToRecyclerView(recyclerView); updateBadge(); + + Disposable disposable = settingsNotificationManager.listenForNotificationUpdates() + .subscribe(activeNotifications -> { + drawerAdapter.onNotificationsChanged(); + }, (error) -> { + Logger.e(TAG, "Unknown error from SettingsNotificationManager", error); + }); + + compositeDisposable.add(disposable); } @Override public void onDestroy() { super.onDestroy(); + compositeDisposable.clear(); recyclerView.setAdapter(null); EventBus.getDefault().unregister(this); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/FiltersController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/FiltersController.java index b709d81789..8aa4843981 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/FiltersController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/FiltersController.java @@ -44,7 +44,6 @@ import com.github.adamantcheese.chan.ui.layout.FilterLayout; import com.github.adamantcheese.chan.ui.theme.ThemeHelper; import com.github.adamantcheese.chan.ui.toolbar.ToolbarMenuItem; -import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ReportProblemController.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ReportProblemController.kt index 892fdfdaa4..8b9877868a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ReportProblemController.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ReportProblemController.kt @@ -1,6 +1,7 @@ package com.github.adamantcheese.chan.ui.controller import android.content.Context +import com.github.adamantcheese.chan.R import com.github.adamantcheese.chan.controller.Controller import com.github.adamantcheese.chan.ui.layout.ReportProblemLayout @@ -9,6 +10,9 @@ class ReportProblemController(context: Context) private var loadingViewController: LoadingViewController? = null override fun onCreate() { + super.onCreate() + navigation.setTitle(R.string.report_controller_report_an_error_problem) + view = ReportProblemLayout(context).apply { onReady(this@ReportProblemController) } @@ -33,6 +37,6 @@ class ReportProblemController(context: Context) } override fun onFinished() { - this.stopPresenting() + this.navigationController.popController() } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/crashlogs/ReviewCrashLogsController.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/crashlogs/ReviewCrashLogsController.kt new file mode 100644 index 0000000000..07994cd395 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/crashlogs/ReviewCrashLogsController.kt @@ -0,0 +1,46 @@ +package com.github.adamantcheese.chan.ui.controller.crashlogs + +import android.content.Context +import com.github.adamantcheese.chan.R +import com.github.adamantcheese.chan.controller.Controller +import com.github.adamantcheese.chan.ui.controller.LoadingViewController +import com.github.adamantcheese.chan.ui.layout.crashlogs.CrashLog +import com.github.adamantcheese.chan.ui.layout.crashlogs.ReviewCrashLogsLayout +import com.github.adamantcheese.chan.ui.layout.crashlogs.ReviewCrashLogsLayoutCallbacks + +class ReviewCrashLogsController(context: Context) : Controller(context), ReviewCrashLogsLayoutCallbacks { + private var loadingViewController: LoadingViewController? = null + + override fun onCreate() { + super.onCreate() + navigation.setTitle(R.string.review_crashlogs_controller_title) + + view = ReviewCrashLogsLayout(context).apply { onCreate(this@ReviewCrashLogsController) } + } + + override fun onDestroy() { + super.onDestroy() + + (view as ReviewCrashLogsLayout).onDestroy() + } + + override fun showProgressDialog() { + hideProgressDialog() + + loadingViewController = LoadingViewController(context, true) + presentController(loadingViewController) + } + + override fun hideProgressDialog() { + loadingViewController?.stopPresenting() + loadingViewController = null + } + + override fun onCrashLogClicked(crashLog: CrashLog) { + navigationController.pushController(ViewFullCrashLogController(context, crashLog)) + } + + override fun onFinished() { + navigationController.popController() + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/crashlogs/ViewFullCrashLogController.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/crashlogs/ViewFullCrashLogController.kt new file mode 100644 index 0000000000..48cac2f7d2 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/crashlogs/ViewFullCrashLogController.kt @@ -0,0 +1,31 @@ +package com.github.adamantcheese.chan.ui.controller.crashlogs + +import android.content.Context +import com.github.adamantcheese.chan.controller.Controller +import com.github.adamantcheese.chan.ui.layout.crashlogs.CrashLog +import com.github.adamantcheese.chan.ui.layout.crashlogs.ViewFullCrashLogLayout + +class ViewFullCrashLogController( + context: Context, + private val crashLog: CrashLog +) : Controller(context), ViewFullCrashLogLayout.ViewFullCrashLogLayoutCallbacks { + + override fun onCreate() { + super.onCreate() + navigation.setTitle(crashLog.fileName) + + view = ViewFullCrashLogLayout(context, crashLog).apply { + onCreate(this@ViewFullCrashLogController) + } + } + + override fun onDestroy() { + super.onDestroy() + + (view as ViewFullCrashLogLayout).onDestroy() + } + + override fun onFinished() { + navigationController.popController() + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/DeveloperSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/DeveloperSettingsController.java index 2a0b0ac8e7..f1f5ef7530 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/DeveloperSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/DeveloperSettingsController.java @@ -23,6 +23,7 @@ import android.widget.ScrollView; import android.widget.TextView; +import com.github.adamantcheese.chan.BuildConfig; import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.controller.Controller; @@ -32,6 +33,7 @@ import com.github.adamantcheese.chan.core.manager.FilterWatchManager; import com.github.adamantcheese.chan.core.manager.WakeManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.core.settings.state.PersistableChanState; import com.github.adamantcheese.chan.ui.controller.LogsController; import com.github.adamantcheese.chan.utils.Logger; @@ -44,6 +46,7 @@ import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.Chan.instance; +import static com.github.adamantcheese.chan.core.settings.ChanSettings.NO_HASH_SET; import static com.github.adamantcheese.chan.utils.AndroidUtils.dp; import static com.github.adamantcheese.chan.utils.AndroidUtils.getAttrColor; import static com.github.adamantcheese.chan.utils.AndroidUtils.showToast; @@ -152,7 +155,7 @@ public void onCreate() { || t.getName().equalsIgnoreCase("Profile Saver") || t.getName().contains("Okio") || t.getName().contains("AsyncTask")) - //@formatter:on + //@formatter:on continue; StackTraceElement[] elements = t.getStackTrace(); Logger.i("STACKDUMP-HEADER", "Thread: " + t.getName()); @@ -194,6 +197,28 @@ public void onCreate() { addCrashOnSafeThrowButton(wrapper); + // Reset the hash and make the app updated + Button resetPrevApkHash = new Button(context); + resetPrevApkHash.setOnClickListener(v -> { + ChanSettings.previousDevHash.setSync(NO_HASH_SET); + ChanSettings.updateCheckTime.setSync(0L); + PersistableChanState.setHasNewApkUpdateSync(false); + ((StartActivity) context).restartApp(); + }); + resetPrevApkHash.setText("Make app updated"); + wrapper.addView(resetPrevApkHash); + + // Set hash to current and trigger the update check + Button setCurrentApkHashAsPrevApkHash = new Button(context); + setCurrentApkHashAsPrevApkHash.setOnClickListener(v -> { + ChanSettings.previousDevHash.setSync(BuildConfig.COMMIT_HASH); + ChanSettings.updateCheckTime.setSync(0L); + PersistableChanState.setHasNewApkUpdateSync(true); + ((StartActivity) context).restartApp(); + }); + setCurrentApkHashAsPrevApkHash.setText("Make app not updated"); + wrapper.addView(setCurrentApkHashAsPrevApkHash); + ScrollView scrollView = new ScrollView(context); scrollView.addView(wrapper); view = scrollView; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/MainSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/MainSettingsController.java index e73bee0b7f..c22dafb452 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/MainSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/MainSettingsController.java @@ -16,23 +16,32 @@ */ package com.github.adamantcheese.chan.ui.controller.settings; +import android.app.AlertDialog; import android.content.Context; +import android.view.ViewGroup; import com.github.adamantcheese.chan.BuildConfig; import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; +import com.github.adamantcheese.chan.core.manager.ReportManager; import com.github.adamantcheese.chan.core.presenter.SettingsPresenter; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.controller.FiltersController; import com.github.adamantcheese.chan.ui.controller.LicensesController; import com.github.adamantcheese.chan.ui.controller.ReportProblemController; import com.github.adamantcheese.chan.ui.controller.SitesSetupController; +import com.github.adamantcheese.chan.ui.controller.crashlogs.ReviewCrashLogsController; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType; +import com.github.adamantcheese.chan.ui.settings.SettingView; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; +import com.github.adamantcheese.chan.utils.Logger; import javax.inject.Inject; +import io.reactivex.disposables.Disposable; + import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getApplicationLabel; import static com.github.adamantcheese.chan.utils.AndroidUtils.getIsOfficial; @@ -43,12 +52,19 @@ public class MainSettingsController extends SettingsController implements SettingsPresenter.Callback { + private static final String TAG = "MainSettingsController"; + @Inject private SettingsPresenter presenter; + @Inject + ReportManager reportManager; private LinkSettingView watchLink; private LinkSettingView sitesSetting; private LinkSettingView filtersSetting; + private LinkSettingView updateSettingView; + private LinkSettingView reportSettingView; + private BooleanSettingView collectCrashLogsSettingView; public MainSettingsController(Context context) { super(context); @@ -64,6 +80,13 @@ public void onCreate() { populatePreferences(); buildPreferences(); + Disposable disposable = settingsNotificationManager.listenForNotificationUpdates() + .subscribe((event) -> onNotificationsChanged(), (error) -> { + Logger.e(TAG, "Unknown error received from SettingsNotificationManager", error); + }); + + compositeDisposable.add(disposable); + presenter.create(this); } @@ -93,6 +116,41 @@ public void setWatchEnabled(boolean enabled) { : R.string.setting_watch_summary_disabled); } + private void onNotificationsChanged() { + Logger.d(TAG, "onNotificationsChanged called"); + + updateSettingNotificationIcon( + settingsNotificationManager.getOrDefault(SettingNotificationType.ApkUpdate), + getViewGroupOrThrow(updateSettingView) + ); + updateSettingNotificationIcon( + settingsNotificationManager.getOrDefault(SettingNotificationType.CrashLog), + getViewGroupOrThrow(reportSettingView) + ); + } + + @Override + public void onPreferenceChange(SettingView item) { + super.onPreferenceChange(item); + + if (item == collectCrashLogsSettingView) { + if (!ChanSettings.collectCrashLogs.get()) { + // If disabled delete all already collected crash logs to cancel the notification + // (if it's shown) and to avoid showing notification afterwards. + + reportManager.deleteAllCrashLogs(); + } + } + } + + private ViewGroup getViewGroupOrThrow(SettingView settingView) { + if (!(settingView.getView() instanceof ViewGroup)) { + throw new IllegalStateException("updateSettingView must have ViewGroup attached to it"); + } + + return (ViewGroup) settingView.getView(); + } + private void populatePreferences() { // General group { @@ -157,20 +215,9 @@ private void populatePreferences() { private void setupAboutGroup() { SettingsGroup about = new SettingsGroup(R.string.settings_group_about); - about.add(new LinkSettingView(this, - getApplicationLabel() + " " + BuildConfig.VERSION_NAME + " " + (getIsOfficial() ? "✓" : "✗"), - "Tap to check for updates", - v -> ((StartActivity) context).getUpdateManager().manualUpdateCheck() - )); - - about.add(new LinkSettingView(this, R.string.settings_report, R.string.settings_report_description, v -> { - navigationController.presentController(new ReportProblemController(context)); - })); - about.add(new BooleanSettingView(this, - ChanSettings.autoCrashLogsUpload, - R.string.settings_auto_crash_report, - R.string.settings_auto_crash_report_description - )); + about.add(createUpdateSettingView()); + about.add(createReportSettingView()); + about.add(createCollectCrashLogsSettingView()); about.add(new LinkSettingView(this, "Find " + getApplicationLabel() + " on GitHub", @@ -204,4 +251,73 @@ private void setupAboutGroup() { groups.add(about); } + + private BooleanSettingView createCollectCrashLogsSettingView() { + collectCrashLogsSettingView = new BooleanSettingView(this, + ChanSettings.collectCrashLogs, + R.string.settings_collect_crash_logs, + R.string.settings_collect_crash_logs_description + ); + + return collectCrashLogsSettingView; + } + + private LinkSettingView createReportSettingView() { + reportSettingView = new LinkSettingView( + this, + R.string.settings_report, + R.string.settings_report_description, v -> { + onReportSettingClick(); + }); + + reportSettingView.setSettingNotificationType(SettingNotificationType.CrashLog); + return reportSettingView; + } + + private void onReportSettingClick() { + int crashLogsCount = reportManager.countCrashLogs(); + + if (crashLogsCount > 0) { + suggestReviewingCrashLogs(crashLogsCount); + return; + } + + openReportProblemController(); + } + + private void suggestReviewingCrashLogs(int crashLogsCount) { + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.settings_report_suggest_sending_logs_title, crashLogsCount)) + .setMessage(R.string.settings_report_suggest_sending_logs_message) + .setPositiveButton(R.string.settings_report_review_button_text, (dialog, which) -> { + openReviewCrashLogsController(); + }) + .setNeutralButton(R.string.settings_report_review_later_button_text, (dialog, which) -> openReportProblemController()) + .setNegativeButton(R.string.settings_report_delete_all_crash_logs, (dialog, which) -> { + reportManager.deleteAllCrashLogs(); + openReportProblemController(); + }) + .create(); + + alertDialog.show(); + } + + private void openReviewCrashLogsController() { + navigationController.pushController(new ReviewCrashLogsController(context)); + } + + private void openReportProblemController() { + navigationController.pushController(new ReportProblemController(context)); + } + + private LinkSettingView createUpdateSettingView() { + updateSettingView = new LinkSettingView(this, + getApplicationLabel() + " " + BuildConfig.VERSION_NAME + " " + (getIsOfficial() ? "✓" : "✗"), + "Tap to check for updates", + v -> ((StartActivity) context).getUpdateManager().manualUpdateCheck() + ); + + updateSettingView.setSettingNotificationType(SettingNotificationType.ApkUpdate); + return updateSettingView; + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/SettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/SettingsController.java index 8edaaa63f1..9c38f03b9f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/SettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/settings/SettingsController.java @@ -18,19 +18,24 @@ import android.content.Context; import android.content.res.Configuration; +import android.graphics.PorterDuff; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.appcompat.widget.AppCompatImageView; + import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.controller.Controller; +import com.github.adamantcheese.chan.core.manager.SettingsNotificationManager; import com.github.adamantcheese.chan.ui.helper.RefreshUIMessage; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; import com.github.adamantcheese.chan.ui.settings.IntegerSettingView; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; import com.github.adamantcheese.chan.ui.settings.ListSettingView; +import com.github.adamantcheese.chan.ui.settings.SettingNotificationType; import com.github.adamantcheese.chan.ui.settings.SettingView; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; import com.github.adamantcheese.chan.ui.settings.StringSettingView; @@ -39,30 +44,40 @@ import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import io.reactivex.disposables.CompositeDisposable; + import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.dp; import static com.github.adamantcheese.chan.utils.AndroidUtils.findViewsById; import static com.github.adamantcheese.chan.utils.AndroidUtils.inflate; import static com.github.adamantcheese.chan.utils.AndroidUtils.isTablet; import static com.github.adamantcheese.chan.utils.AndroidUtils.postToEventBus; +import static com.github.adamantcheese.chan.utils.AndroidUtils.updatePaddings; import static com.github.adamantcheese.chan.utils.AndroidUtils.waitForLayout; public class SettingsController extends Controller implements AndroidUtils.OnMeasuredCallback { + + @Inject + protected SettingsNotificationManager settingsNotificationManager; + protected LinearLayout content; protected List groups = new ArrayList<>(); - protected List requiresUiRefresh = new ArrayList<>(); - // Very user unfriendly. protected List requiresRestart = new ArrayList<>(); - + protected CompositeDisposable compositeDisposable = new CompositeDisposable(); private boolean needRestart = false; public SettingsController(Context context) { super(context); + + inject(this); } @Override @@ -72,9 +87,15 @@ public void onShow() { waitForLayout(view, this); } + @Override + public void onCreate() { + super.onCreate(); + } + @Override public void onDestroy() { super.onDestroy(); + compositeDisposable.clear(); if (needRestart) { ((StartActivity) context).restartApp(); @@ -188,6 +209,40 @@ protected void buildPreferences() { } } + protected void updateSettingNotificationIcon( + SettingNotificationType settingNotificationType, + ViewGroup preferenceView + ) { + AppCompatImageView notificationIcon = + preferenceView.findViewById(R.id.setting_notification_icon); + + if (notificationIcon != null) { + updatePaddings(notificationIcon, dp(16), dp(16), -1, -1); + } + + boolean hasNotifications + = settingsNotificationManager.hasNotifications(settingNotificationType); + + if (settingNotificationType != SettingNotificationType.Default && hasNotifications) { + if (notificationIcon == null) { + throw new NullPointerException("SettingNotificationType is not default (" + + settingNotificationType + + "), but notificationIcon was not found!"); + } + + int tintColor = context.getResources().getColor( + settingNotificationType.getNotificationIconTintColor() + ); + + notificationIcon.setColorFilter(tintColor, PorterDuff.Mode.SRC_IN); + notificationIcon.setVisibility(VISIBLE); + } else { + if (notificationIcon != null) { + notificationIcon.setVisibility(GONE); + } + } + } + private void setDescriptionText(View view, String topText, String bottomText) { ((TextView) view.findViewById(R.id.top)).setText(topText); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/ReportProblemLayout.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/ReportProblemLayout.kt index a7f4a642a6..a39f238c94 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/ReportProblemLayout.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/ReportProblemLayout.kt @@ -30,19 +30,17 @@ class ReportProblemLayout(context: Context) : FrameLayout(context), ReportProble private val reportActivityProblemDescription: TextInputEditText private val reportActivityAttachLogsButton: AppCompatCheckBox private val reportActivityLogsText: TextInputEditText - private val reportActivityCancel: AppCompatButton private val reportActivitySendReport: AppCompatButton init { inject(this) - inflate(context, R.layout.activity_report, this).apply { - reportActivityProblemTitle = findViewById(R.id.report_activity_problem_title) - reportActivityProblemDescription = findViewById(R.id.report_activity_problem_description) - reportActivityAttachLogsButton = findViewById(R.id.report_activity_attach_logs_button) - reportActivityLogsText = findViewById(R.id.report_activity_logs_text) - reportActivityCancel = findViewById(R.id.report_activity_cancel) - reportActivitySendReport = findViewById(R.id.report_activity_send_report) + inflate(context, R.layout.layout_report, this).apply { + reportActivityProblemTitle = findViewById(R.id.report_controller_problem_title) + reportActivityProblemDescription = findViewById(R.id.report_controller_problem_description) + reportActivityAttachLogsButton = findViewById(R.id.report_controller_attach_logs_button) + reportActivityLogsText = findViewById(R.id.report_controller_logs_text) + reportActivitySendReport = findViewById(R.id.report_controller_send_report) } } @@ -57,7 +55,6 @@ class ReportProblemLayout(context: Context) : FrameLayout(context), ReportProble reportActivityAttachLogsButton.setOnCheckedChangeListener { _, isChecked -> reportActivityLogsText.isEnabled = isChecked } - reportActivityCancel.setOnClickListener { callbacks?.onFinished() } reportActivitySendReport.setOnClickListener { onSendReportClick() } this.callbacks = controllerCallbacks @@ -78,7 +75,7 @@ class ReportProblemLayout(context: Context) : FrameLayout(context), ReportProble val logs = reportActivityLogsText.text?.toString() ?: "" if (title.isEmpty()) { - reportActivityProblemTitle.error = getString(R.string.report_activity_title_cannot_be_empty_error) + reportActivityProblemTitle.error = getString(R.string.report_controller_title_cannot_be_empty_error) return } @@ -86,12 +83,12 @@ class ReportProblemLayout(context: Context) : FrameLayout(context), ReportProble description.isEmpty() && !(reportActivityAttachLogsButton.isChecked && logs.isNotEmpty()) ) { - reportActivityProblemDescription.error = getString(R.string.report_activity_description_cannot_be_empty_error) + reportActivityProblemDescription.error = getString(R.string.report_controller_description_cannot_be_empty_error) return } if (reportActivityAttachLogsButton.isChecked && logs.isEmpty()) { - reportActivityLogsText.error = getString(R.string.report_activity_logs_are_empty_error) + reportActivityLogsText.error = getString(R.string.report_controller_logs_are_empty_error) return } @@ -113,7 +110,7 @@ class ReportProblemLayout(context: Context) : FrameLayout(context), ReportProble val errorMessage = error.message ?: "No error message" val formattedMessage = getString( - R.string.report_activity_error_while_trying_to_send_report, + R.string.report_controller_error_while_trying_to_send_report, errorMessage ) @@ -125,13 +122,13 @@ class ReportProblemLayout(context: Context) : FrameLayout(context), ReportProble private fun handleResult(result: ModularResult) { when (result) { is ModularResult.Value -> { - showToast(context, R.string.report_activity_report_sent_message) + showToast(context, R.string.report_controller_report_sent_message) callbacks?.onFinished() } is ModularResult.Error -> { val errorMessage = result.error.message ?: "No error message" val formattedMessage = getString( - R.string.report_activity_error_while_trying_to_send_report, + R.string.report_controller_error_while_trying_to_send_report, errorMessage ) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLog.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLog.kt new file mode 100644 index 0000000000..591cc86ad5 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLog.kt @@ -0,0 +1,24 @@ +package com.github.adamantcheese.chan.ui.layout.crashlogs + +import java.io.File + +data class CrashLog(val file: File, val fileName: String, var markedToSend: Boolean) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CrashLog + + if (fileName != other.fileName) return false + if (markedToSend != other.markedToSend) return false + + return true + } + + override fun hashCode(): Int { + var result = fileName.hashCode() + result = 31 * result + markedToSend.hashCode() + return result + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLogsListArrayAdapter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLogsListArrayAdapter.kt new file mode 100644 index 0000000000..7862cec17f --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLogsListArrayAdapter.kt @@ -0,0 +1,94 @@ +package com.github.adamantcheese.chan.ui.layout.crashlogs + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.FrameLayout +import android.widget.TextView +import androidx.appcompat.widget.AppCompatCheckBox +import com.github.adamantcheese.chan.R + +internal class CrashLogsListArrayAdapter( + context: Context, + crashLogs: List, + private val callbacks: CrashLogsListCallbacks +) : ArrayAdapter(context, R.layout.cell_crashlog_item) { + private val inflater: LayoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private val handler = Handler(Looper.getMainLooper()) + + init { + clear() + addAll(crashLogs) + } + + @SuppressLint("ViewHolder") + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val crashLog = checkNotNull(getItem(position)) { + "Item with position $position is null! Items count = ${count}" + } + + val cellView = inflater.inflate(R.layout.cell_crashlog_item, parent, false) + val fileNameView = cellView.findViewById(R.id.cell_crashlog_file_name) + val checkBox = cellView.findViewById(R.id.cell_crashlog_send_checkbox) + val clickArea = cellView.findViewById(R.id.cell_crashlog_click_area) + + fileNameView.text = crashLog.fileName + checkBox.isChecked = crashLog.markedToSend + + fileNameView.setOnClickListener { + callbacks.onCrashLogClicked(crashLog) + } + clickArea.setOnClickListener { + val crashLogItem = getItem(position) + ?: return@setOnClickListener + + crashLogItem.markedToSend = !crashLogItem.markedToSend + checkBox.isChecked = crashLogItem.markedToSend + + handler.removeCallbacksAndMessages(null) + + // Wait 100ms so that we have a little bit of time to show ripple effect + handler.postDelayed({ notifyDataSetChanged() }, 100) + } + + return cellView + } + + fun updateAll() { + notifyDataSetChanged() + } + + fun deleteSelectedCrashLogs(selectedCrashLogs: List): Int { + if (selectedCrashLogs.isNotEmpty()) { + selectedCrashLogs.forEach { crashLog -> remove(crashLog) } + notifyDataSetChanged() + } + + return count + } + + fun getSelectedCrashLogs(): List { + val selectedCrashLogs = mutableListOf() + + for (i in 0 until count) { + val item = getItem(i) + ?: continue + + if (item.markedToSend) { + selectedCrashLogs += item + } + } + + return selectedCrashLogs + } + + fun onDestroy() { + handler.removeCallbacksAndMessages(null) + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLogsListCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLogsListCallbacks.kt new file mode 100644 index 0000000000..cf02dd7998 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/CrashLogsListCallbacks.kt @@ -0,0 +1,5 @@ +package com.github.adamantcheese.chan.ui.layout.crashlogs + +internal interface CrashLogsListCallbacks { + fun onCrashLogClicked(crashLog: CrashLog) +} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ReviewCrashLogsLayout.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ReviewCrashLogsLayout.kt new file mode 100644 index 0000000000..c185d56c2d --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ReviewCrashLogsLayout.kt @@ -0,0 +1,117 @@ +package com.github.adamantcheese.chan.ui.layout.crashlogs + +import android.content.Context +import android.widget.FrameLayout +import android.widget.ListView +import androidx.appcompat.widget.AppCompatButton +import com.github.adamantcheese.chan.Chan.inject +import com.github.adamantcheese.chan.R +import com.github.adamantcheese.chan.core.manager.ReportManager +import com.github.adamantcheese.chan.utils.AndroidUtils.getString +import com.github.adamantcheese.chan.utils.AndroidUtils.showToast +import com.github.adamantcheese.chan.utils.Logger +import com.github.adamantcheese.chan.utils.plusAssign +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + + +internal class ReviewCrashLogsLayout(context: Context) : FrameLayout(context), CrashLogsListCallbacks { + + @Inject + lateinit var reportManager: ReportManager + + private lateinit var compositeDisposable: CompositeDisposable + private var callbacks: ReviewCrashLogsLayoutCallbacks? = null + private val crashLogsList: ListView + private val deleteCrashLogsButton: AppCompatButton + private val sendCrashLogsButton: AppCompatButton + + init { + inject(this) + + inflate(context, R.layout.controller_review_crashlogs, this).apply { + crashLogsList = findViewById(R.id.review_crashlogs_controller_crashlogs_list) + deleteCrashLogsButton = findViewById(R.id.review_crashlogs_controller_delete_crashlogs_button) + sendCrashLogsButton = findViewById(R.id.review_crashlogs_controller_send_crashlogs_button) + + val crashLogs = reportManager.getCrashLogs() + .map { crashLogFile -> CrashLog(crashLogFile, crashLogFile.name, false) } + + val adapter = CrashLogsListArrayAdapter( + context, + crashLogs, + this@ReviewCrashLogsLayout + ) + + crashLogsList.adapter = adapter + adapter.updateAll() + + deleteCrashLogsButton.setOnClickListener { onDeleteCrashLogsButtonClicked(adapter) } + sendCrashLogsButton.setOnClickListener { onSendCrashLogsButtonClicked(adapter) } + } + } + + private fun onDeleteCrashLogsButtonClicked(adapter: CrashLogsListArrayAdapter) { + val selectedCrashLogs = adapter.getSelectedCrashLogs() + if (selectedCrashLogs.isEmpty()) { + return + } + + reportManager.deleteCrashLogs(selectedCrashLogs) + + val newCrashLogsAmount = adapter.deleteSelectedCrashLogs(selectedCrashLogs) + if (newCrashLogsAmount == 0) { + callbacks?.onFinished() + } + + showToast(context, getString(R.string.deleted_n_crashlogs, selectedCrashLogs.size)) + } + + private fun onSendCrashLogsButtonClicked(adapter: CrashLogsListArrayAdapter) { + val selectedCrashLogs = adapter.getSelectedCrashLogs() + if (selectedCrashLogs.isEmpty()) { + return + } + + compositeDisposable += reportManager.sendCrashLogs(selectedCrashLogs) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { callbacks?.showProgressDialog() } + .subscribe({ + callbacks?.hideProgressDialog() + + if (selectedCrashLogs.size == adapter.count) { + callbacks?.onFinished() + } else { + adapter.deleteSelectedCrashLogs(selectedCrashLogs) + } + + showToast(context, getString(R.string.sent_n_crashlogs, selectedCrashLogs.size)) + }, { error -> + val message = "Error while trying to send logs: ${error.message}" + Logger.e(TAG, message, error) + showToast(context, message) + + callbacks?.hideProgressDialog() + }) + } + + fun onCreate(callbacks: ReviewCrashLogsLayoutCallbacks) { + this.callbacks = callbacks + this.compositeDisposable = CompositeDisposable() + } + + fun onDestroy() { + callbacks = null + compositeDisposable.dispose() + (crashLogsList.adapter as CrashLogsListArrayAdapter).onDestroy() + } + + override fun onCrashLogClicked(crashLog: CrashLog) { + callbacks?.onCrashLogClicked(crashLog) + } + + companion object { + private const val TAG = "ReviewCrashLogsLayout" + } +} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ReviewCrashLogsLayoutCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ReviewCrashLogsLayoutCallbacks.kt new file mode 100644 index 0000000000..64cfb41829 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ReviewCrashLogsLayoutCallbacks.kt @@ -0,0 +1,8 @@ +package com.github.adamantcheese.chan.ui.layout.crashlogs + +internal interface ReviewCrashLogsLayoutCallbacks { + fun onCrashLogClicked(crashLog: CrashLog) + fun showProgressDialog() + fun hideProgressDialog() + fun onFinished() +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ViewFullCrashLogLayout.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ViewFullCrashLogLayout.kt new file mode 100644 index 0000000000..0e9fe933ff --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/layout/crashlogs/ViewFullCrashLogLayout.kt @@ -0,0 +1,47 @@ +package com.github.adamantcheese.chan.ui.layout.crashlogs + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.FrameLayout +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatEditText +import com.github.adamantcheese.chan.R + +@SuppressLint("ViewConstructor") +class ViewFullCrashLogLayout(context: Context, private val crashLog: CrashLog) : FrameLayout(context) { + + private var callbacks: ViewFullCrashLogLayoutCallbacks? = null + + private val crashLogText: AppCompatEditText + private val save: AppCompatButton + + init { + inflate(context, R.layout.layout_view_full_crashlog, this).apply { + crashLogText = findViewById(R.id.view_full_crashlog_text) + save = findViewById(R.id.view_full_crashlog_save) + crashLogText.setText(crashLog.file.readText()) + + save.setOnClickListener { + val text = crashLogText.text.toString() + + if (text.isNotEmpty()) { + crashLog.file.writeText(text) + } + + callbacks?.onFinished() + } + } + } + + fun onCreate(callbacks: ViewFullCrashLogLayoutCallbacks) { + this.callbacks = callbacks + } + + fun onDestroy() { + this.callbacks = null + } + + interface ViewFullCrashLogLayoutCallbacks { + fun onFinished() + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingNotificationType.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingNotificationType.kt new file mode 100644 index 0000000000..bb862751a4 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingNotificationType.kt @@ -0,0 +1,19 @@ +package com.github.adamantcheese.chan.ui.settings + +import androidx.annotation.ColorInt +import com.github.adamantcheese.chan.R + +enum class SettingNotificationType(@ColorInt val notificationIconTintColor: Int) { + /** + * No active notification + * */ + Default(android.R.color.transparent), + /** + * New apk update is available notification + * */ + ApkUpdate(R.color.new_apk_update_icon_color), + /** + * There is at least one crash log available notification + * */ + CrashLog(R.color.new_crash_log_icon_color) +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingView.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingView.java index 3ee73d95d1..4204231910 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingView.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/SettingView.java @@ -25,6 +25,7 @@ public abstract class SettingView { public final String name; public View view; public View divider; + public SettingNotificationType settingNotificationType = SettingNotificationType.Default; public SettingView(SettingsController settingsController, String name) { this.settingsController = settingsController; @@ -35,9 +36,17 @@ public void setView(View view) { this.view = view; } + public View getView() { + return view; + } + public void setEnabled(boolean enabled) { } + public void setSettingNotificationType(SettingNotificationType type) { + this.settingNotificationType = type; + } + public String getTopDescription() { return name; } @@ -45,4 +54,8 @@ public String getTopDescription() { public String getBottomDescription() { return null; } + + public SettingNotificationType getSettingNotificationType() { + return settingNotificationType; + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/toolbar/NavigationItem.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/toolbar/NavigationItem.java index a31d42a2a3..ea5da0e5ee 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/toolbar/NavigationItem.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/toolbar/NavigationItem.java @@ -53,6 +53,10 @@ public void setTitle(int resId) { title = getString(resId); } + public void setTitle(String title) { + this.title = title; + } + public MenuBuilder buildMenu() { return new MenuBuilder(this); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageViewGestureDetector.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageViewGestureDetector.kt index 39e94bcac7..49597d6ec1 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageViewGestureDetector.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageViewGestureDetector.kt @@ -5,7 +5,6 @@ import android.view.MotionEvent import android.view.View import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.adamantcheese.chan.utils.AndroidUtils.dp -import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ui.PlayerView import pl.droidsonroids.gif.GifDrawable import pl.droidsonroids.gif.GifImageView diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/AndroidUtils.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/AndroidUtils.java index 879c374bcf..84ec606989 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/AndroidUtils.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/AndroidUtils.java @@ -72,11 +72,13 @@ import static android.content.Context.CLIPBOARD_SERVICE; import static android.content.Context.INPUT_METHOD_SERVICE; import static android.content.Context.JOB_SCHEDULER_SERVICE; +import static android.content.Context.MODE_PRIVATE; import static android.content.Context.NOTIFICATION_SERVICE; import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; public class AndroidUtils { private static final String TAG = "AndroidUtils"; + private static final String CHAN_STATE_PREFS_NAME = "chan_state"; @SuppressLint("StaticFieldLeak") private static Application application; @@ -127,6 +129,10 @@ public static SharedPreferences getPreferences() { return PreferenceManager.getDefaultSharedPreferences(application); } + public static SharedPreferences getAppState() { + return getAppContext().getSharedPreferences(CHAN_STATE_PREFS_NAME, MODE_PRIVATE); + } + public static boolean getIsOfficial() { try { @SuppressLint("PackageManagerGetSignatures") @@ -290,6 +296,30 @@ public static void requestViewAndKeyboardFocus(View view) { } } + public static void updatePaddings(View view, int left, int right, int top, int bottom) { + int newLeft = left; + if (newLeft < 0) { + newLeft = view.getPaddingLeft(); + } + + int newRight = right; + if (newRight < 0) { + newRight = view.getPaddingRight(); + } + + int newTop = top; + if (newTop < 0) { + newTop = view.getPaddingTop(); + } + + int newBottom = bottom; + if (newBottom < 0) { + newBottom = view.getPaddingBottom(); + } + + view.setPadding(newLeft, newTop, newRight, newBottom); + } + public interface OnMeasuredCallback { /** * Called when the layout is done. diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/KtExtensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/KtExtensions.kt index 84139e709a..e59bd2578a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/KtExtensions.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/KtExtensions.kt @@ -1,7 +1,14 @@ package com.github.adamantcheese.chan.utils +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + /** * Forces the kotlin compiler to require handling of all branches in the "when" operator * */ val T.exhaustive: T - get() = this \ No newline at end of file + get() = this + +operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + this.add(disposable) +} \ No newline at end of file diff --git a/Kuroba/app/src/main/res/drawable/ic_setting_alert.xml b/Kuroba/app/src/main/res/drawable/ic_setting_alert.xml new file mode 100644 index 0000000000..93db179177 --- /dev/null +++ b/Kuroba/app/src/main/res/drawable/ic_setting_alert.xml @@ -0,0 +1,5 @@ + + + diff --git a/Kuroba/app/src/main/res/layout/cell_crashlog_item.xml b/Kuroba/app/src/main/res/layout/cell_crashlog_item.xml new file mode 100644 index 0000000000..f94b7faa16 --- /dev/null +++ b/Kuroba/app/src/main/res/layout/cell_crashlog_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Kuroba/app/src/main/res/layout/cell_link.xml b/Kuroba/app/src/main/res/layout/cell_link.xml index cfd1e46330..21949c008d 100644 --- a/Kuroba/app/src/main/res/layout/cell_link.xml +++ b/Kuroba/app/src/main/res/layout/cell_link.xml @@ -44,4 +44,11 @@ along with this program. If not, see . android:textColor="?text_color_primary" android:textSize="14sp" /> + + + + diff --git a/Kuroba/app/src/main/res/layout/controller_review_crashlogs.xml b/Kuroba/app/src/main/res/layout/controller_review_crashlogs.xml new file mode 100644 index 0000000000..d487f6e0bf --- /dev/null +++ b/Kuroba/app/src/main/res/layout/controller_review_crashlogs.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Kuroba/app/src/main/res/layout/activity_report.xml b/Kuroba/app/src/main/res/layout/layout_report.xml similarity index 66% rename from Kuroba/app/src/main/res/layout/activity_report.xml rename to Kuroba/app/src/main/res/layout/layout_report.xml index e738c82299..fed1357592 100644 --- a/Kuroba/app/src/main/res/layout/activity_report.xml +++ b/Kuroba/app/src/main/res/layout/layout_report.xml @@ -2,14 +2,14 @@ @@ -20,29 +20,7 @@ android:orientation="vertical"> - - - - - - @@ -69,7 +47,7 @@ @@ -120,13 +98,13 @@ app:counterMaxLength="65535"> @@ -143,29 +121,19 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - - + android:text="@string/report_controller_send_report" /> diff --git a/Kuroba/app/src/main/res/layout/layout_view_full_crashlog.xml b/Kuroba/app/src/main/res/layout/layout_view_full_crashlog.xml new file mode 100644 index 0000000000..c7fcb78c79 --- /dev/null +++ b/Kuroba/app/src/main/res/layout/layout_view_full_crashlog.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Kuroba/app/src/main/res/layout/setting_boolean.xml b/Kuroba/app/src/main/res/layout/setting_boolean.xml index 3f97f666ae..4606353fe5 100644 --- a/Kuroba/app/src/main/res/layout/setting_boolean.xml +++ b/Kuroba/app/src/main/res/layout/setting_boolean.xml @@ -38,6 +38,9 @@ along with this program. If not, see . android:id="@+id/switcher" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginRight="16dp" /> + android:layout_marginEnd="16dp" /> + + + diff --git a/Kuroba/app/src/main/res/layout/setting_link.xml b/Kuroba/app/src/main/res/layout/setting_link.xml index 8d7176311f..7b5fd5e97b 100644 --- a/Kuroba/app/src/main/res/layout/setting_link.xml +++ b/Kuroba/app/src/main/res/layout/setting_link.xml @@ -23,6 +23,23 @@ along with this program. If not, see . android:paddingBottom="16dp" android:paddingTop="16dp"> - + + + + + + + + + + + diff --git a/Kuroba/app/src/main/res/layout/setting_notification.xml b/Kuroba/app/src/main/res/layout/setting_notification.xml new file mode 100644 index 0000000000..37d4c79e12 --- /dev/null +++ b/Kuroba/app/src/main/res/layout/setting_notification.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index b7a8b34ed0..595dcbe9df 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -91,8 +91,6 @@ along with this program. If not, see . Delete Undo Reset - Mark as my post - Unmark as my post Done Continue Create @@ -100,6 +98,11 @@ along with this program. If not, see . Move Yes No + Save + Send + + Mark as my post + Unmark as my post App settings Grant @@ -763,19 +766,19 @@ Don't have a 4chan Pass?
The standard Lorem Ipsum passage, used since the 1500s "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - Report an error/crash or other problem - I have a problem with: - Problem description: - Attach logs (You can remove some parts of them you don\'t want to send): - No logs - Cancel - Send report - Title cannot be empty! - If description is empty, logs cannot be empty! - \"Attach logs\" option is checked, but logs are empty! - Sending report… - Sent \;) - Error while trying to send report: %1$s + Report an error/crash/other problem + I have a problem with: + Problem description: + Attach logs (You can remove some parts of them you don\'t want to send): + No logs + Cancel + Send report + Title cannot be empty! + If description is empty, logs cannot be empty! + \"Attach logs\" option is checked, but logs are empty! + Sending report… + Sent \;) + Error while trying to send report: %1$s HSID SSID @@ -793,8 +796,8 @@ Don't have a 4chan Pass?
Mock reply Report Report a problem/crash - Automatic crash reporting - By enabling this setting, all collected crash logs will be uploaded automatically on every app restart. Crash reports only collect the crash log itself, app version and basic OS information. + Collect crash logs + By enabling this setting, the app will start collecting crash logs. They are NOT uploaded automatically. Crash logs only collect the stacktrace itself, app version and basic OS information. You can review and upload them manually (or delete) by clicking the \"Report\" setting. Base directory reset to default Could not create default base dir: %1$s @@ -809,7 +812,15 @@ Don't have a 4chan Pass?
Apk successfully copied Show copy apk dialog when downloading an update Every time you download a new update a dialog with suggestion to copy that apk to some other directory will be shown - + Review crash logs + You have %1$d collected crash logs + Would you like to review and maybe send them to developers? + Review crash logs + Review later + Delete all crash logs + Deleted %1$d crash log(s) + Sent %1$d crash log(s) + Couldn\'t initialize captcha, reason: %1$s WebView is not installed Some part of WebViewChromium couldn\'t get initialized: (%1$s) diff --git a/Kuroba/app/src/main/res/values/styles.xml b/Kuroba/app/src/main/res/values/styles.xml index ad55104ff4..10fc5ce814 100644 --- a/Kuroba/app/src/main/res/values/styles.xml +++ b/Kuroba/app/src/main/res/values/styles.xml @@ -18,6 +18,8 @@ along with this program. If not, see . #4caf50 #388e3c #009688 + #008080 + #E54545