refactor(android)!: restructure to standard Capacitor plugin layout

Restructure Android project from nested module layout to standard
Capacitor plugin structure following community conventions.

Structure Changes:
- Move plugin code from android/plugin/ to android/src/main/java/
- Move test app from android/app/ to test-apps/android-test-app/app/
- Remove nested android/plugin module structure
- Remove nested android/app test app structure

Build Infrastructure:
- Add Gradle wrapper files (gradlew, gradlew.bat, gradle/wrapper/)
- Transform android/build.gradle from root project to library module
- Update android/settings.gradle for standalone plugin builds
- Add android/gradle.properties with AndroidX configuration
- Add android/consumer-rules.pro for ProGuard rules

Configuration Updates:
- Add prepare script to package.json for automatic builds on npm install
- Update package.json version to 1.0.1
- Update android/build.gradle to properly resolve Capacitor dependencies
- Update test-apps/android-test-app/settings.gradle with correct paths
- Remove android/variables.gradle (hardcode values in build.gradle)

Documentation:
- Update BUILDING.md with new structure and build process
- Update INTEGRATION_GUIDE.md to reflect standard structure
- Update README.md to remove path fix warnings
- Add test-apps/BUILD_PROCESS.md documenting test app build flows

Test App Configuration:
- Fix android-test-app to correctly reference plugin and Capacitor
- Remove capacitor-cordova-android-plugins dependency (not needed)
- Update capacitor.settings.gradle path verification in fix script

BREAKING CHANGE: Plugin now uses standard Capacitor Android structure.
Consuming apps must update their capacitor.settings.gradle to reference
android/ instead of android/plugin/. This is automatically handled by
Capacitor CLI for apps using standard plugin installation.
This commit is contained in:
Matthew Raymer
2025-11-05 08:08:37 +00:00
parent c4b7f6382f
commit d9bdeb6d02
128 changed files with 1654 additions and 1747 deletions

View File

@@ -0,0 +1,193 @@
package com.timesafari.dailynotification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.util.Log;
/**
* Manages notification channels and ensures they are properly configured
* for reliable notification delivery.
*
* Handles channel creation, importance checking, and provides deep links
* to channel settings when notifications are blocked.
*
* @author Matthew Raymer
* @version 1.0
*/
public class ChannelManager {
private static final String TAG = "ChannelManager";
private static final String DEFAULT_CHANNEL_ID = "timesafari.daily";
private static final String DEFAULT_CHANNEL_NAME = "Daily Notifications";
private static final String DEFAULT_CHANNEL_DESCRIPTION = "Daily notifications from TimeSafari";
private final Context context;
private final NotificationManager notificationManager;
public ChannelManager(Context context) {
this.context = context;
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Ensures the default notification channel exists and is properly configured.
* Creates the channel if it doesn't exist.
*
* @return true if channel is ready for notifications, false if blocked
*/
public boolean ensureChannelExists() {
try {
Log.d(TAG, "Ensuring notification channel exists");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID);
if (channel == null) {
Log.d(TAG, "Creating notification channel");
createDefaultChannel();
return true;
} else {
Log.d(TAG, "Channel exists with importance: " + channel.getImportance());
return channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
}
} else {
// Pre-Oreo: channels don't exist, always ready
Log.d(TAG, "Pre-Oreo device, channels not applicable");
return true;
}
} catch (Exception e) {
Log.e(TAG, "Error ensuring channel exists", e);
return false;
}
}
/**
* Checks if the notification channel is enabled and can deliver notifications.
*
* @return true if channel is enabled, false if blocked
*/
public boolean isChannelEnabled() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID);
if (channel == null) {
Log.w(TAG, "Channel does not exist");
return false;
}
int importance = channel.getImportance();
Log.d(TAG, "Channel importance: " + importance);
return importance != NotificationManager.IMPORTANCE_NONE;
} else {
// Pre-Oreo: always enabled
return true;
}
} catch (Exception e) {
Log.e(TAG, "Error checking channel status", e);
return false;
}
}
/**
* Gets the current channel importance level.
*
* @return importance level, or -1 if channel doesn't exist
*/
public int getChannelImportance() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID);
if (channel != null) {
return channel.getImportance();
}
}
return -1;
} catch (Exception e) {
Log.e(TAG, "Error getting channel importance", e);
return -1;
}
}
/**
* Opens the notification channel settings for the user to enable notifications.
*
* @return true if settings intent was launched, false otherwise
*/
public boolean openChannelSettings() {
try {
Log.d(TAG, "Opening channel settings");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName())
.putExtra(Settings.EXTRA_CHANNEL_ID, DEFAULT_CHANNEL_ID)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
Log.d(TAG, "Channel settings opened");
return true;
} else {
Log.d(TAG, "Channel settings not available on pre-Oreo");
return false;
}
} catch (Exception e) {
Log.e(TAG, "Error opening channel settings", e);
return false;
}
}
/**
* Creates the default notification channel with high importance.
*/
private void createDefaultChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
DEFAULT_CHANNEL_ID,
DEFAULT_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(DEFAULT_CHANNEL_DESCRIPTION);
channel.enableLights(true);
channel.enableVibration(true);
channel.setShowBadge(true);
notificationManager.createNotificationChannel(channel);
Log.d(TAG, "Default channel created with HIGH importance");
}
}
/**
* Gets the default channel ID for use in notifications.
*
* @return the default channel ID
*/
public String getDefaultChannelId() {
return DEFAULT_CHANNEL_ID;
}
/**
* Logs the current channel status for debugging.
*/
public void logChannelStatus() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID);
if (channel != null) {
Log.i(TAG, "Channel Status - ID: " + channel.getId() +
", Importance: " + channel.getImportance() +
", Enabled: " + (channel.getImportance() != NotificationManager.IMPORTANCE_NONE));
} else {
Log.w(TAG, "Channel does not exist");
}
} else {
Log.i(TAG, "Pre-Oreo device, channels not applicable");
}
} catch (Exception e) {
Log.e(TAG, "Error logging channel status", e);
}
}
}