-
Notifications
You must be signed in to change notification settings - Fork 224
feat(Instagram): Add Focus Lock patch
#1928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
...s/instagram/src/main/java/app/morphe/extension/instagram/patches/focusLock/FocusLock.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /* | ||
| * Copyright (C) 2026 piko <https://github.com/crimera/piko> | ||
| * | ||
| * See the included NOTICE file for GPLv3 §7(b) terms that apply to this code. | ||
| */ | ||
|
|
||
| package app.morphe.extension.instagram.patches.focusLock; | ||
|
|
||
| import static app.morphe.extension.instagram.utils.IgStr.str; | ||
|
|
||
| import android.text.format.DateFormat; | ||
|
|
||
| import java.util.Date; | ||
|
|
||
| import app.morphe.extension.instagram.utils.Pref; | ||
|
|
||
| /** | ||
| * Commitment mode: once locked, the selected distraction-free protections are forced on | ||
| * and cannot be turned off until the lock expires. Unlocking early requires a cooling-off | ||
| * period, so the decision to bring Reels back cannot be made on impulse. | ||
| */ | ||
| @SuppressWarnings("unused") | ||
| public class FocusLock { | ||
| public static final long COOLING_OFF_MS = 24L * 60 * 60 * 1000; | ||
| private static final long DAY_MS = 24L * 60 * 60 * 1000; | ||
|
|
||
| private static long parseLong(String value) { | ||
| try { | ||
| return Long.parseLong(value); | ||
| } catch (Exception ignored) { | ||
| return 0L; | ||
| } | ||
| } | ||
|
|
||
| public static long lockedUntil() { | ||
| return parseLong(Pref.focusLockUntil()); | ||
| } | ||
|
|
||
| public static long unlockRequestedAt() { | ||
| return parseLong(Pref.focusLockUnlockRequestedAt()); | ||
| } | ||
|
|
||
| // Intentionally does not consult SettingsStatus: the lock timestamp is only ever written by | ||
| // this class, and hooks like HideNavigationButtonsPatch may read preferences at class-load time. | ||
| public static boolean isLocked() { | ||
| return System.currentTimeMillis() < lockedUntil(); | ||
| } | ||
|
|
||
| /** True while an unlock request is pending its cooling-off period. */ | ||
| public static boolean isUnlockPending() { | ||
| return isLocked() && unlockRequestedAt() > 0; | ||
| } | ||
|
|
||
| public static long unlockAvailableAt() { | ||
| return unlockRequestedAt() + COOLING_OFF_MS; | ||
| } | ||
|
|
||
| public static boolean canUnlockNow() { | ||
| return isUnlockPending() && System.currentTimeMillis() >= unlockAvailableAt(); | ||
| } | ||
|
|
||
| // Enforcement helpers. These are OR-ed into the regular preference getters in Pref, | ||
| // so the underlying switches keep their stored value and simply cannot take effect. | ||
| public static boolean blocksReels() { | ||
| return isLocked() && Pref.focusLockBlockReels(); | ||
| } | ||
|
|
||
| public static boolean blocksExplore() { | ||
| return isLocked() && Pref.focusLockBlockExplore(); | ||
| } | ||
|
|
||
| public static boolean lock() { | ||
| long days = parseLong(Pref.focusLockDurationDays()); | ||
| if (days <= 0) days = 7; | ||
| long until = System.currentTimeMillis() + days * DAY_MS; | ||
| return Pref.setFocusLockUntil(String.valueOf(until)) | ||
| && Pref.setFocusLockUnlockRequestedAt("0"); | ||
| } | ||
|
|
||
| public static boolean requestUnlock() { | ||
| return Pref.setFocusLockUnlockRequestedAt(String.valueOf(System.currentTimeMillis())); | ||
| } | ||
|
|
||
| public static boolean cancelUnlockRequest() { | ||
| return Pref.setFocusLockUnlockRequestedAt("0"); | ||
| } | ||
|
|
||
| public static boolean unlock() { | ||
| if (!canUnlockNow()) return false; | ||
| return Pref.setFocusLockUntil("0") | ||
| && Pref.setFocusLockUnlockRequestedAt("0"); | ||
| } | ||
|
|
||
| public static String formatDate(long millis) { | ||
| return DateFormat.format("yyyy-MM-dd HH:mm", new Date(millis)).toString(); | ||
| } | ||
|
|
||
| /** Summary shown under the lock/unlock button. */ | ||
| public static String statusSummary() { | ||
| if (!isLocked()) { | ||
| return str("piko_focus_lock_status_unlocked"); | ||
| } | ||
| String until = formatDate(lockedUntil()); | ||
| if (canUnlockNow()) { | ||
| return str("piko_focus_lock_status_unlock_ready"); | ||
| } | ||
| if (isUnlockPending()) { | ||
| return String.format(str("piko_focus_lock_status_unlock_pending"), formatDate(unlockAvailableAt()), until); | ||
| } | ||
| return String.format(str("piko_focus_lock_status_locked"), until); | ||
| } | ||
|
|
||
| public static String buttonTitle() { | ||
| if (!isLocked()) return str("piko_focus_lock_button_lock"); | ||
| if (canUnlockNow()) return str("piko_focus_lock_button_unlock"); | ||
| if (isUnlockPending()) return str("piko_focus_lock_button_cancel_unlock"); | ||
| return str("piko_focus_lock_button_request_unlock"); | ||
| } | ||
| } |
96 changes: 96 additions & 0 deletions
96
...gram/src/main/java/app/morphe/extension/instagram/patches/focusLock/FocusLockDialogs.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| /* | ||
| * Copyright (C) 2026 piko <https://github.com/crimera/piko> | ||
| * | ||
| * See the included NOTICE file for GPLv3 §7(b) terms that apply to this code. | ||
| */ | ||
|
|
||
| package app.morphe.extension.instagram.patches.focusLock; | ||
|
|
||
| import static app.morphe.extension.instagram.utils.IgStr.str; | ||
|
|
||
| import android.app.AlertDialog; | ||
| import android.content.Context; | ||
| import android.preference.Preference; | ||
|
|
||
| import app.morphe.extension.crimera.PikoUtils; | ||
| import app.morphe.extension.instagram.utils.Pref; | ||
| import app.morphe.extension.instagram.settings.preference.widgets.InstagramPreferenceStyle; | ||
| import app.morphe.extension.shared.Utils; | ||
|
|
||
| /** Confirmation flows behind the Focus Lock button. */ | ||
| public class FocusLockDialogs { | ||
|
|
||
| public static void onActionPressed(Context context, Preference button) { | ||
| if (!FocusLock.isLocked()) { | ||
| confirmLock(context); | ||
| } else if (FocusLock.canUnlockNow()) { | ||
| confirmUnlock(context); | ||
| } else if (FocusLock.isUnlockPending()) { | ||
| FocusLock.cancelUnlockRequest(); | ||
| refresh(button); | ||
| } else { | ||
| confirmRequestUnlock(context, button); | ||
| } | ||
| } | ||
|
|
||
| private static void refresh(Preference button) { | ||
| button.setTitle(FocusLock.buttonTitle()); | ||
| button.setSummary(FocusLock.statusSummary()); | ||
| } | ||
|
|
||
| private static void confirmLock(Context context) { | ||
| boolean reels = Pref.focusLockBlockReels(); | ||
| boolean explore = Pref.focusLockBlockExplore(); | ||
| if (!reels && !explore) { | ||
| PikoUtils.toast(str("piko_focus_lock_nothing_selected")); | ||
| return; | ||
| } | ||
| String days = Pref.focusLockDurationDays(); | ||
| String message = String.format( | ||
| str("piko_focus_lock_confirm_lock_desc"), | ||
| days, | ||
| FocusLock.COOLING_OFF_MS / (60 * 60 * 1000) | ||
| ); | ||
| new AlertDialog.Builder(InstagramPreferenceStyle.dialogContext(context)) | ||
| .setTitle(str("piko_focus_lock_confirm_lock")) | ||
| .setMessage(message) | ||
| .setNegativeButton(str("piko_cancel"), null) | ||
| .setPositiveButton(str("piko_ok"), (d, w) -> { | ||
| if (FocusLock.lock()) { | ||
| PikoUtils.toast(str("piko_focus_lock_locked_toast")); | ||
| // Some hooks read preferences once at class-load time, so restart. | ||
| Utils.restartApp(Utils.getContext()); | ||
| } | ||
| }) | ||
| .show(); | ||
| } | ||
|
|
||
| private static void confirmRequestUnlock(Context context, Preference button) { | ||
| String message = String.format( | ||
| str("piko_focus_lock_confirm_request_unlock_desc"), | ||
| FocusLock.COOLING_OFF_MS / (60 * 60 * 1000) | ||
| ); | ||
| new AlertDialog.Builder(InstagramPreferenceStyle.dialogContext(context)) | ||
| .setTitle(str("piko_focus_lock_confirm_request_unlock")) | ||
| .setMessage(message) | ||
| .setNegativeButton(str("piko_cancel"), null) | ||
| .setPositiveButton(str("piko_ok"), (d, w) -> { | ||
| FocusLock.requestUnlock(); | ||
| refresh(button); | ||
| }) | ||
| .show(); | ||
| } | ||
|
|
||
| private static void confirmUnlock(Context context) { | ||
| new AlertDialog.Builder(InstagramPreferenceStyle.dialogContext(context)) | ||
| .setTitle(str("piko_focus_lock_confirm_unlock")) | ||
| .setMessage(str("piko_focus_lock_confirm_unlock_desc")) | ||
| .setNegativeButton(str("piko_cancel"), null) | ||
| .setPositiveButton(str("piko_ok"), (d, w) -> { | ||
| if (FocusLock.unlock()) { | ||
| Utils.restartApp(Utils.getContext()); | ||
| } | ||
| }) | ||
| .show(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.