Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import app.morphe.extension.instagram.entity.MediaData;
import app.morphe.extension.instagram.settings.SettingsStatus;
import app.morphe.extension.instagram.utils.Pref;
import android.util.Log;
import app.morphe.extension.shared.Logger;
import app.morphe.extension.shared.ShareLinkSanitizer;
import app.morphe.extension.shared.Utils;
Expand All @@ -28,6 +29,7 @@
import app.morphe.extension.instagram.patches.story.StorySeenRequestScope;
import app.morphe.extension.crimera.PikoUtils;

import app.morphe.extension.instagram.patches.focusLock.FocusLock;
import app.morphe.extension.instagram.settings.ActivityHook;

@SuppressWarnings("unused")
Expand Down Expand Up @@ -139,7 +141,9 @@ public static void interceptUri(URI uri) throws IOException{
|| path.contains("/discover/topical_explore_stream")
|| (host.contains("i.instagram.com") && path.contains("/fbsearch/recent_searches/"))
|| (host.contains("i.instagram.com") && path.contains("/fbsearch/top_serp/"))) {
shouldBlockUri = DISABLE_EXPLORE;
// Focus Lock is read per request, not from the cached field: a lock that
// expires while the process is alive must release explore without a restart.
shouldBlockUri = DISABLE_EXPLORE || FocusLock.blocksExplore();
} else if (path.contains("/api/v1/media/") && path.contains("comments/")) {
shouldBlockUri = DISABLE_COMMENTS;
} else if (path.contains("/discover/ayml") || path.contains("/discover/chaining")) { // Thanks to @brosssh
Expand All @@ -159,6 +163,9 @@ public static void interceptUri(URI uri) throws IOException{
Logger.printException(() -> "intercept URI failed: ", ex);
}
// Exception is hanndled at call.
if (shouldBlockUri && Pref.pikoDebug()) {
Log.d("piko", "blocked uri: " + uri);
}
if(shouldBlockUri) {
throw new IOException("Block uri");
}
Expand Down
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");
}
}
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import android.content.Intent;

import app.morphe.extension.instagram.patches.focusLock.FocusLock;
import app.morphe.extension.instagram.settings.SettingsRestart;
import app.morphe.extension.instagram.utils.Pref;

Expand Down Expand Up @@ -205,6 +206,20 @@ private static Config loadConfig(Boolean nativeCreateVisible) {
return config;
}

/**
* Focus Lock hides the Reels tab regardless of the stored config.
*
* Applied only where tabs are rendered, never inside {@link #loadConfig()}: the navigation
* editor loads the config too, so filtering there would let pressing OK save the filtered
* layout permanently. The user's own layout stays stored and returns once the lock ends.
*/
private static Config applyFocusLock(Config config) {
if (!FocusLock.blocksReels() || !config.visible().contains(Tab.REELS)) return config;
EnumSet<Tab> visible = EnumSet.copyOf(config.visible());
visible.remove(Tab.REELS);
return normalize(config.order(), visible, config.startup());
}

public static boolean saveConfig(List<Tab> order, Set<Tab> visible, Tab startup) {
Config normalized = normalize(order, visible, startup);
String previous = Pref.navigationTabs();
Expand Down Expand Up @@ -245,7 +260,8 @@ public static List<Object> transformNavigationTabs(
List<Candidate> candidates = classify(allCandidates);
List<Candidate> output = new ArrayList<>();
int knownCapacity = Math.max(0, MAX_TABS - unknown.size());
Config config = loadConfig(find(current, Tab.CREATE, new IdentityHashMap<>()) != null);
Config config = applyFocusLock(
loadConfig(find(current, Tab.CREATE, new IdentityHashMap<>()) != null));
for (Tab tab : config.order()) {
if (output.size() == knownCapacity) break;
if (!config.visible().contains(tab)) continue;
Expand Down Expand Up @@ -295,7 +311,7 @@ public static void applyStartupTab(
try {
if (intent.hasExtra(STARTUP_TAB_EXTRA)) return;
List<Candidate> candidates = classify(allCandidates);
Config config = loadConfig();
Config config = applyFocusLock(loadConfig());
Tab[] choices = startupChoices(config);
for (Tab tab : choices) {
Candidate candidate = find(candidates, tab, new IdentityHashMap<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public class Settings {
public static final BooleanSetting HIDE_GROUP_CREATION_BUTTON_ON_SHARESHEET = new BooleanSetting("hide_group_creation_button_on_sharesheet", true);
public static final BooleanSetting DISABLE_REELS_SCROLLING = new BooleanSetting("disable_reels_scrolling", false);
public static final BooleanSetting DISABLE_SWIPE_TO_CREATE = new BooleanSetting("disable_swipe_to_create", false);
public static final BooleanSetting FOCUS_LOCK_BLOCK_REELS = new BooleanSetting("focus_lock_block_reels", true);
public static final BooleanSetting FOCUS_LOCK_BLOCK_EXPLORE = new BooleanSetting("focus_lock_block_explore", false);
public static final StringSetting FOCUS_LOCK_DURATION_DAYS = new StringSetting("focus_lock_duration_days", "7");
// Epoch millis. "0" means not locked.
public static final StringSetting FOCUS_LOCK_UNTIL = new StringSetting("focus_lock_until", "0");
// Epoch millis of the pending unlock request. "0" means none.
public static final StringSetting FOCUS_LOCK_UNLOCK_REQUESTED_AT = new StringSetting("focus_lock_unlock_requested_at", "0");
public static final BooleanSetting REMOVE_EMPTY_BOTTOM_SPACE = new BooleanSetting("remove_empty_bottom_space", true);
public static final BooleanSetting DISABLE_TYPING_STATUS = new BooleanSetting("disable_typing_status", false);
public static final BooleanSetting HIDE_NOTES_TRAY = new BooleanSetting("hide_notes_tray", false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ public static void disableReelsScrolling() {
public static void disableSwipeToCreate() {
disableSwipeToCreate = true;
}
public static boolean focusLock = false;
public static void focusLock() {
focusLock = true;
}
public static boolean disableDoubleTapLike = false;
public static void disableDoubleTapLike() {
disableDoubleTapLike = true;
Expand All @@ -140,7 +144,7 @@ public static void hideReelsFollowButton() {
hideReelsFollowButton = true;
}
public static boolean distractionFreeSection() {
return (hideReelsFollowButton || disableDoubleTapLike || hideNotesTray || disableHighlights || disableStories || disableExplore || disableComments || hideStoriesTray || limitFollowingFeed || hideGroupCreationOnSharesheet || disableReelsScrolling || disableSwipeToCreate);
return (hideReelsFollowButton || disableDoubleTapLike || hideNotesTray || disableHighlights || disableStories || disableExplore || disableComments || hideStoriesTray || limitFollowingFeed || hideGroupCreationOnSharesheet || disableReelsScrolling || disableSwipeToCreate || focusLock);
}

//Misc section.
Expand Down Expand Up @@ -269,6 +273,7 @@ public static void loadStatusMap(){
FLAGS.put(str("piko_disable_highlights"),SettingsStatus.disableHighlights);
FLAGS.put(str("piko_disable_stories"),SettingsStatus.disableStories);
FLAGS.put(str("piko_disable_swipe_to_create"), SettingsStatus.disableSwipeToCreate);
FLAGS.put(str("piko_focus_lock"), SettingsStatus.focusLock);

FLAGS.put(str("piko_view_dm_anonymously"),SettingsStatus.viewDmAnonymously);
FLAGS.put(str("piko_save_deleted_messages"),SettingsStatus.saveDeletedMessages);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,34 @@ public Preference switchPreference(String title, String summary, BooleanSetting
return preference;
}

/**
* A switch whose value is currently forced on by another feature.
*
* When {@code forced} the switch is shown checked and non-interactive with
* {@code forcedSummary}, without writing to the stored value, so the user's own choice
* returns once the feature releases it.
* Not specific to any patch: any feature that overrides a setting can use it.
*/
public Preference forcedSwitchPreference(
String title,
String summary,
BooleanSetting setting,
boolean forced,
String forcedSummary
) {
Preference preference = switchPreference(title, forced ? forcedSummary : summary, setting);
if (forced && preference instanceof SwitchPref) {
SwitchPref switchPreference = (SwitchPref) preference;
switchPreference.setPersistent(false);
// Attaching the preference initializes it from the default value, which would undo
// setChecked(true), so the default has to be true as well while it is forced.
switchPreference.setDefaultValue(Boolean.TRUE);
switchPreference.setChecked(true);
switchPreference.setSwitchInteractionEnabled(false);
Comment thread
naoverse06 marked this conversation as resolved.
}
return preference;
}

public Preference listPreference(String title, String summary, StringSetting setting) {
ListPref preference = new ListPref(context);
String key = setting.key;
Expand Down
Loading
Loading