/* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.micode.notes.widget; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.util.Log; import android.widget.RemoteViews; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.tool.ResourceParser; import net.micode.notes.ui.NoteEditActivity; import net.micode.notes.ui.NotesListActivity; /** * 笔记小部件基础提供者抽象类 * * 功能概述: * 1. 管理小部件生命周期 * 2. 处理小部件点击事件 * 3. 从数据库加载笔记数据更新小部件视图 * 4. 支持隐私模式显示 * * 子类需要实现以下抽象方法: * - getBgResourceId() : 获取背景资源ID * - getLayoutId() : 获取布局资源ID * - getWidgetType() : 获取小部件类型 */ public abstract class NoteWidgetProvider extends AppWidgetProvider { // 数据库查询字段 private static final String[] PROJECTION = new String[] { NoteColumns.ID, NoteColumns.BG_COLOR_ID, NoteColumns.SNIPPET, NoteColumns.WIDGET_ID // 添加字段用于数据一致性检查 }; // 查询字段索引 private static final int COLUMN_ID = 0; private static final int COLUMN_BG_COLOR_ID = 1; private static final int COLUMN_SNIPPET = 2; private static final int COLUMN_WIDGET_ID = 3; // 添加的索引 private static final String TAG = "NoteWidgetProvider"; // ========================== 生命周期方法 ========================== /** * 当小部件被删除时调用 - 清理相关笔记的widget_id字段 * * @param context 应用上下文 * @param appWidgetIds 被删除的小部件ID数组 */ @Override public void onDeleted(Context context, int[] appWidgetIds) { try { // 防止空指针异常 if (context == null || appWidgetIds == null || appWidgetIds.length == 0) { Log.w(TAG, "无效的小部件删除请求"); return; } ContentValues values = new ContentValues(); values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); for (int widgetId : appWidgetIds) { // 使用try-catch防止个别更新失败中断整个操作 try { int rowsUpdated = context.getContentResolver().update( Notes.CONTENT_NOTE_URI, values, NoteColumns.WIDGET_ID + "=?", new String[] { String.valueOf(widgetId) } ); Log.d(TAG, "已清理小部件 " + widgetId + " 关联的笔记引用,影响行数: " + rowsUpdated); } catch (Exception e) { Log.e(TAG, "清理小部件 " + widgetId + " 关联的笔记引用时出错", e); } } } catch (Exception e) { Log.e(TAG, "小部件删除处理发生未预期错误", e); } } // ========================== 核心功能方法 ========================== /** * 获取与指定小部件关联的笔记信息 * * @param context 应用上下文 * @param widgetId 小部件ID * @return 笔记数据Cursor(需在调用处关闭) */ protected Cursor getNoteWidgetInfo(Context context, int widgetId) { if (context == null || widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { return null; } String selection = NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?"; String[] selectionArgs = new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLDER) // 修正: TRASH_FOLER -> TRASH_FOLDER }; try { return context.getContentResolver().query( Notes.CONTENT_NOTE_URI, PROJECTION, selection, selectionArgs, null ); } catch (Exception e) { Log.e(TAG, "查询小部件 " + widgetId + " 关联笔记信息时出错", e); return null; } } /** * 更新小部件显示 - 公开方法供外部调用 * * @param context 应用上下文 * @param appWidgetManager 小部件管理器 * @param appWidgetIds 要更新的小部件ID数组 */ protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { update(context, appWidgetManager, appWidgetIds, false); } /** * 更新小部件显示(支持隐私模式)- 内部实现 * * @param context 应用上下文 * @param appWidgetManager 小部件管理器 * @param appWidgetIds 要更新的小部件ID数组 * @param privacyMode 是否为隐私模式(隐藏真实内容) */ private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, boolean privacyMode) { if (context == null || appWidgetManager == null || appWidgetIds == null) { Log.w(TAG, "无效的更新请求参数"); return; } for (int widgetId : appWidgetIds) { // 跳过无效小部件ID if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { continue; } // 初始化默认值 int bgId = ResourceParser.NoteColor.YELLOW.id; String snippet = ""; long noteId = Notes.DEFAULT_NOTE_ID; // 准备笔记编辑意图 Intent noteIntent = new Intent(context, NoteEditActivity.class); noteIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); noteIntent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, widgetId); noteIntent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 查询数据库获取笔记信息 try (Cursor cursor = getNoteWidgetInfo(context, widgetId)) { if (cursor != null && cursor.moveToFirst()) { // 安全检查:确保一个WidgetID只关联一个笔记 if (cursor.getCount() > 1) { Log.w(TAG, "小部件 " + widgetId + " 关联多个笔记,请检查数据一致性"); // 清除异常关联的笔记 cleanInvalidWidgetLinks(context, widgetId, cursor); // 只处理第一条记录 } // 获取笔记数据 noteId = cursor.getLong(COLUMN_ID); snippet = cursor.getString(COLUMN_SNIPPET); bgId = cursor.getInt(COLUMN_BG_COLOR_ID); noteIntent.putExtra(Intent.EXTRA_UID, noteId); noteIntent.setAction(Intent.ACTION_VIEW); // 检查数据一致性 int storedWidgetId = cursor.getInt(COLUMN_WIDGET_ID); if (storedWidgetId != widgetId) { Log.w(TAG, String.format( "笔记%d的小部件ID不一致: 数据库=%d, 请求=%d", noteId, storedWidgetId, widgetId )); } } else { // 未找到关联笔记 snippet = context.getString(R.string.widget_no_content); noteIntent.setAction(Intent.ACTION_INSERT_OR_EDIT); } } catch (Exception e) { Log.e(TAG, "处理小部件 " + widgetId + " 时发生错误", e); snippet = context.getString(R.string.widget_error_loading); } // 创建小部件视图 RemoteViews remoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); // 设置背景 int bgResId = getBgResourceId(bgId); remoteViews.setImageViewResource(R.id.widget_bg_image, bgResId); noteIntent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); // 创建点击意图 PendingIntent pendingIntent; if (privacyMode) { // 隐私模式:显示提示文本,点击进入主列表 remoteViews.setTextViewText(R.id.widget_text, context.getString(R.string.widget_privacy_mode)); Intent listIntent = new Intent(context, NotesListActivity.class); pendingIntent = PendingIntent.getActivity( context, widgetId, listIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); } else { // 正常模式:显示笔记摘要,点击进入笔记编辑 remoteViews.setTextViewText(R.id.widget_text, snippet); pendingIntent = PendingIntent.getActivity( context, widgetId, noteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); } // 设置点击事件 remoteViews.setOnClickPendingIntent(R.id.widget_text, pendingIntent); // 应用更新 try { appWidgetManager.updateAppWidget(widgetId, remoteViews); Log.d(TAG, "成功更新小部件: " + widgetId); } catch (Exception e) { Log.e(TAG, "更新小部件 " + widgetId + " 失败", e); } } } // ========================== 抽象方法 ========================== /** * 获取背景资源ID * * @param bgId 背景颜色ID * @return 对应的drawable资源ID */ protected abstract int getBgResourceId(int bgId); /** * 获取小部件布局资源ID * * @return 布局资源ID */ protected abstract int getLayoutId(); /** * 获取小部件类型 * * @return 小部件类型常量 */ protected abstract int getWidgetType(); // ========================== 新增功能方法 ========================== /** * 清理无效的小部件关联(数据一致性修复) * * @param context 应用上下文 * @param widgetId 小部件ID * @param cursor 查询到的关联笔记数据 */ private void cleanInvalidWidgetLinks(Context context, int widgetId, Cursor cursor) { if (cursor == null) return; ContentValues values = new ContentValues(); values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); try { // 遍历所有关联笔记 cursor.moveToPosition(-1); // 移动到开始位置 while (cursor.moveToNext()) { long currentNoteId = cursor.getLong(COLUMN_ID); int currentWidgetId = cursor.getInt(COLUMN_WIDGET_ID); // 只保留最新关联的笔记 if (currentWidgetId == widgetId) { Log.i(TAG, "保留笔记 " + currentNoteId + " 关联的小部件 " + widgetId); continue; } // 解除其他笔记的关联 Uri noteUri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, currentNoteId); int rows = context.getContentResolver().update( noteUri, values, null, null); if (rows > 0) { Log.w(TAG, "已清理笔记 " + currentNoteId + " 上的小部件关联"); } } } catch (Exception e) { Log.e(TAG, "清理无效小部件关联时出错", e); } } // ========================== 新增功能:定时刷新支持 ========================== /** * 设置小部件自动刷新间隔 * * @param context 应用上下文 * @param appWidgetId 小部件ID * @param intervalMinutes 刷新间隔(分钟) */ public static void setUpdateInterval(Context context, int appWidgetId, int intervalMinutes) { // 最小间隔限制 if (intervalMinutes < 15) { Log.w(TAG, "刷新间隔不能小于15分钟"); intervalMinutes = 60; // 默认1小时 } // TODO: 实现AlarmManager定时刷新逻辑 } // ========================== 新增功能:多尺寸小部件支持 ========================== /** * 小部件尺寸类型 */ public enum WidgetSize { SIZE_1X1, SIZE_2X1, SIZE_2X2, SIZE_3X3 } /** * 获取当前小部件尺寸 * * @param context 应用上下文 * @param appWidgetId 小部件ID * @return 小部件尺寸类型 */ protected WidgetSize getWidgetSize(Context context, int appWidgetId) { // TODO: 实现小部件尺寸检测逻辑 return WidgetSize.SIZE_2X1; // 默认返回2x1 } }