1+ package org .wally .waller ;
2+
3+ import android .app .NotificationChannel ;
4+ import android .app .NotificationManager ;
5+ import android .app .PendingIntent ;
6+ import android .content .Context ;
7+ import android .content .Intent ;
8+ import android .content .SharedPreferences ;
9+ import android .content .pm .PackageInfo ;
10+ import android .content .pm .PackageManager ;
11+ import android .net .ConnectivityManager ;
12+ import android .net .NetworkInfo ;
13+ import android .os .Build ;
14+ import android .util .Log ;
15+
16+ import androidx .core .app .NotificationCompat ;
17+
18+ import org .json .JSONObject ;
19+
20+ import java .io .BufferedReader ;
21+ import java .io .InputStreamReader ;
22+ import java .net .HttpURLConnection ;
23+ import java .net .URL ;
24+
25+ public class UpdateNotifier {
26+
27+ private static final String TAG = "UpdateNotifier" ;
28+ private static final String PREFS_NAME = "update_checker_prefs" ;
29+ private static final String KEY_LAST_NOTIFIED = "last_notified_timestamp" ;
30+ private static final long SEVEN_DAYS_MS = 7L * 24 * 60 * 60 * 1000 ;
31+ private static final String CHANNEL_ID = "update_channel" ;
32+ private static final String CHANNEL_NAME = "App Updates" ;
33+ private static final int NOTIFICATION_ID = 999 ;
34+ private static final String API_URL = "https://api.github.com/repos/Fector101/wallpaper-carousel/releases/latest" ;
35+
36+ public static boolean isCooldownActive (Context context ) {
37+ SharedPreferences prefs = context .getSharedPreferences (PREFS_NAME , Context .MODE_PRIVATE );
38+ long lastNotified = prefs .getLong (KEY_LAST_NOTIFIED , 0 );
39+ long now = System .currentTimeMillis ();
40+ Log .d (TAG , "elapsed=" + (now - lastNotified ) + "ms, cooldown=" + SEVEN_DAYS_MS + "ms" );
41+ return now - lastNotified < SEVEN_DAYS_MS ;
42+ }
43+
44+ public static boolean isNetworkAvailable (Context context ) {
45+ try {
46+ ConnectivityManager cm = (ConnectivityManager ) context .getSystemService (Context .CONNECTIVITY_SERVICE );
47+ if (cm == null ) return false ;
48+ NetworkInfo info = cm .getActiveNetworkInfo ();
49+ return info != null && info .isConnected ();
50+ } catch (Exception e ) {
51+ Log .e (TAG , "Error checking network" , e );
52+ return false ;
53+ }
54+ }
55+
56+ /**
57+ * Full check: version compare + notes fetch + notification.
58+ * Returns true if the check should be retried later (fetch failed), false otherwise.
59+ */
60+ public static boolean checkAndNotify (Context context ) {
61+ try {
62+ String currentVersion = getCurrentVersion (context );
63+ Log .d (TAG , "Current version: " + currentVersion );
64+
65+ String latestVersion = fetchLatestVersion ();
66+ if (latestVersion == null ) {
67+ Log .e (TAG , "Failed to fetch latest version" );
68+ return true ;
69+ }
70+ Log .d (TAG , "Latest version: " + latestVersion );
71+
72+ if (latestVersion .equals (currentVersion )) {
73+ Log .d (TAG , "Already on latest version" );
74+ return false ;
75+ }
76+
77+ Log .d (TAG , "New version available: " + latestVersion );
78+ String releaseNotes = fetchReleaseNotes (latestVersion );
79+ boolean posted = sendNotification (context , latestVersion , releaseNotes );
80+ if (!posted ) {
81+ Log .w (TAG , "Notification not eligible to post, cooldown timestamp NOT saved" );
82+ return true ;
83+ }
84+
85+ context .getSharedPreferences (PREFS_NAME , Context .MODE_PRIVATE )
86+ .edit ().putLong (KEY_LAST_NOTIFIED , System .currentTimeMillis ()).apply ();
87+ Log .d (TAG , "Timestamp saved, notification sent" );
88+
89+ } catch (Exception e ) {
90+ Log .e (TAG , "Update check failed" , e );
91+ return true ;
92+ }
93+ return false ;
94+ }
95+
96+ public static String getCurrentVersion (Context context ) {
97+ try {
98+ PackageInfo pInfo = context .getPackageManager ().getPackageInfo (context .getPackageName (), 0 );
99+ return pInfo .versionName ;
100+ } catch (PackageManager .NameNotFoundException e ) {
101+ Log .e (TAG , "Could not get package version" , e );
102+ return "" ;
103+ }
104+ }
105+
106+ private static String fetchLatestVersion () {
107+ try {
108+ URL url = new URL (API_URL );
109+ HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
110+ conn .setRequestMethod ("GET" );
111+ conn .setRequestProperty ("Accept" , "application/vnd.github.v3+json" );
112+ conn .setConnectTimeout (10000 );
113+ conn .setReadTimeout (10000 );
114+
115+ int responseCode = conn .getResponseCode ();
116+ if (responseCode != 200 ) {
117+ Log .e (TAG , "HTTP " + responseCode );
118+ return null ;
119+ }
120+
121+ BufferedReader reader = new BufferedReader (new InputStreamReader (conn .getInputStream ()));
122+ StringBuilder sb = new StringBuilder ();
123+ String line ;
124+ while ((line = reader .readLine ()) != null ) {
125+ sb .append (line );
126+ }
127+ reader .close ();
128+ conn .disconnect ();
129+
130+ JSONObject json = new JSONObject (sb .toString ());
131+ String tag = json .getString ("tag_name" );
132+ return tag .startsWith ("v" ) ? tag .substring (1 ) : tag ;
133+
134+ } catch (Exception e ) {
135+ Log .e (TAG , "Failed to fetch latest version" , e );
136+ return null ;
137+ }
138+ }
139+
140+ private static String fetchReleaseNotes (String version ) {
141+ try {
142+ String fileUrl = "https://github.com/Fector101/wallpaper-carousel/releases/download/v"
143+ + version + "/update-note-v" + version + ".txt" ;
144+ URL url = new URL (fileUrl );
145+ HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
146+ conn .setRequestMethod ("GET" );
147+ conn .setConnectTimeout (10000 );
148+ conn .setReadTimeout (10000 );
149+
150+ int responseCode = conn .getResponseCode ();
151+ if (responseCode != 200 ) {
152+ Log .e (TAG , "Release notes HTTP " + responseCode );
153+ return null ;
154+ }
155+
156+ BufferedReader reader = new BufferedReader (new InputStreamReader (conn .getInputStream ()));
157+ StringBuilder sb = new StringBuilder ();
158+ String line ;
159+ while ((line = reader .readLine ()) != null ) {
160+ sb .append (line );
161+ sb .append ("\n " );
162+ }
163+ reader .close ();
164+ conn .disconnect ();
165+ return sb .toString ().trim ();
166+
167+ } catch (Exception e ) {
168+ Log .e (TAG , "Failed to fetch release notes" , e );
169+ return null ;
170+ }
171+ }
172+
173+ private static boolean canPostNotification (Context context ) {
174+ try {
175+ NotificationManager nm = (NotificationManager ) context .getSystemService (Context .NOTIFICATION_SERVICE );
176+ if (nm == null ) {
177+ Log .w (TAG , "NotificationManager unavailable, cannot post" );
178+ return false ;
179+ }
180+
181+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
182+ if (context .checkSelfPermission (android .Manifest .permission .POST_NOTIFICATIONS )
183+ != android .content .pm .PackageManager .PERMISSION_GRANTED ) {
184+ Log .w (TAG , "POST_NOTIFICATIONS permission not granted" );
185+ return false ;
186+ }
187+ } else if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
188+ if (!nm .areNotificationsEnabled ()) {
189+ Log .w (TAG , "Notifications disabled for app" );
190+ return false ;
191+ }
192+ }
193+
194+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
195+ NotificationChannel channel = new NotificationChannel (
196+ CHANNEL_ID , CHANNEL_NAME , NotificationManager .IMPORTANCE_HIGH );
197+ channel .setDescription ("Notifications for app updates" );
198+ nm .createNotificationChannel (channel );
199+
200+ NotificationChannel created = nm .getNotificationChannel (CHANNEL_ID );
201+ if (created == null || created .getImportance () == NotificationManager .IMPORTANCE_NONE ) {
202+ Log .w (TAG , "Update channel disabled, cannot post" );
203+ return false ;
204+ }
205+ }
206+ return true ;
207+ } catch (Exception e ) {
208+ Log .e (TAG , "Notification eligibility check failed" , e );
209+ return false ;
210+ }
211+ }
212+
213+ private static boolean sendNotification (Context context , String version , String releaseNotes ) {
214+ if (!canPostNotification (context )) {
215+ return false ;
216+ }
217+
218+ NotificationManager nm = (NotificationManager ) context .getSystemService (Context .NOTIFICATION_SERVICE );
219+ if (nm == null ) return false ;
220+
221+ Intent launchIntent = context .getPackageManager ().getLaunchIntentForPackage (context .getPackageName ());
222+ if (launchIntent == null ) return false ;
223+ launchIntent .putExtra ("action" , "open_update" );
224+ launchIntent .putExtra ("version" , version );
225+ launchIntent .putExtra ("release_notes" , releaseNotes );
226+ launchIntent .setFlags (Intent .FLAG_ACTIVITY_NEW_TASK | Intent .FLAG_ACTIVITY_CLEAR_TOP | Intent .FLAG_ACTIVITY_SINGLE_TOP );
227+
228+ PendingIntent pendingIntent = PendingIntent .getActivity (
229+ context , NOTIFICATION_ID , launchIntent ,
230+ PendingIntent .FLAG_UPDATE_CURRENT | PendingIntent .FLAG_IMMUTABLE );
231+
232+ NotificationCompat .Builder builder = new NotificationCompat .Builder (context , CHANNEL_ID )
233+ .setSmallIcon (android .R .drawable .stat_notify_sync )
234+ .setContentTitle ("New version available" )
235+ .setContentText ("v" + version + " is ready. Tap to update." )
236+ .setPriority (NotificationCompat .PRIORITY_HIGH )
237+ .setContentIntent (pendingIntent )
238+ .setAutoCancel (true );
239+
240+ nm .notify (NOTIFICATION_ID , builder .build ());
241+ Log .d (TAG , "Notification sent for v" + version );
242+ return true ;
243+ }
244+ }
0 commit comments