This commit is contained in:
Hellings 2026-10-16 13:05:27 -04:00
parent b8c8ff98e2
commit 1e953b8132
59 changed files with 4278 additions and 3044 deletions

3
.gitignore vendored
View file

@ -65,3 +65,6 @@ skills-lock.json
notable/models/ notable/models/
translator/__pycache__/ translator/__pycache__/
.gradle-user/
.tessl/

View file

@ -1,3 +1,3 @@
# OpenMiInk # OpenMiInk
`OpenMiInk` is a Xiaomi-focused reverse engineering of major mfr's E-Ink controller interfaces under Android. `OpenMiInk` is a Xiaomi-focused reverse engineering including other REs of major mfr's E-Ink controller interfaces under Android.

View file

@ -19,7 +19,45 @@ targetFile = "com/note/fragment/ApplicationFragment.smali"
search = " new-instance v2, Landroidx/recyclerview/widget/GridLayoutManager;\n\n invoke-virtual {p0}, Lcom/note/fragment/ApplicationFragment;->getActivity()Landroidx/fragment/app/FragmentActivity;\n\n move-result-object v3\n\n const/4 v4, 0x4\n\n invoke-direct {v2, v3, v4}, Landroidx/recyclerview/widget/GridLayoutManager;-><init>(Landroid/content/Context;I)V\n\n .line 185\n .local v2, \"layoutManager\":Landroidx/recyclerview/widget/GridLayoutManager;" search = " new-instance v2, Landroidx/recyclerview/widget/GridLayoutManager;\n\n invoke-virtual {p0}, Lcom/note/fragment/ApplicationFragment;->getActivity()Landroidx/fragment/app/FragmentActivity;\n\n move-result-object v3\n\n const/4 v4, 0x4\n\n invoke-direct {v2, v3, v4}, Landroidx/recyclerview/widget/GridLayoutManager;-><init>(Landroid/content/Context;I)V\n\n .line 185\n .local v2, \"layoutManager\":Landroidx/recyclerview/widget/GridLayoutManager;"
replace = " new-instance v2, Landroidx/recyclerview/widget/LinearLayoutManager;\n\n invoke-virtual {p0}, Lcom/note/fragment/ApplicationFragment;->getActivity()Landroidx/fragment/app/FragmentActivity;\n\n move-result-object v3\n\n invoke-direct {v2, v3}, Landroidx/recyclerview/widget/LinearLayoutManager;-><init>(Landroid/content/Context;)V\n\n .line 185\n .local v2, \"layoutManager\":Landroidx/recyclerview/widget/LinearLayoutManager;" replace = " new-instance v2, Landroidx/recyclerview/widget/LinearLayoutManager;\n\n invoke-virtual {p0}, Lcom/note/fragment/ApplicationFragment;->getActivity()Landroidx/fragment/app/FragmentActivity;\n\n move-result-object v3\n\n invoke-direct {v2, v3}, Landroidx/recyclerview/widget/LinearLayoutManager;-><init>(Landroid/content/Context;)V\n\n .line 185\n .local v2, \"layoutManager\":Landroidx/recyclerview/widget/LinearLayoutManager;"
# All external launches funnel through XiaomiProfile.safeStart (null-proof,
# ActivityNotFound-proof): a toast instead of a crash when a target such as
# WPS, Notes, or Settings is missing, disabled, or shadowed by a rename.
[[smaliPatches]]
targetFile = "com/note/utils/AppUtils.smali"
search = " invoke-virtual {p0, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V"
replace = " invoke-static {p0, v0}, Lrip/crit/ms/launcher/XiaomiProfile;->safeStart(Landroid/content/Context;Landroid/content/Intent;)V"
[[smaliPatches]]
targetFile = "com/note/launcher/MainActivity$2.smali"
search = " invoke-virtual {v1, v0}, Lcom/note/launcher/MainActivity;->startActivity(Landroid/content/Intent;)V"
replace = " invoke-static {v1, v0}, Lrip/crit/ms/launcher/XiaomiProfile;->safeStart(Landroid/content/Context;Landroid/content/Intent;)V"
[[smaliPatches]]
targetFile = "com/note/utils/DocMsgManager.smali"
search = " invoke-virtual {v1, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V"
replace = " invoke-static {v1, v0}, Lrip/crit/ms/launcher/XiaomiProfile;->safeStart(Landroid/content/Context;Landroid/content/Intent;)V"
[[smaliPatches]]
targetFile = "com/note/utils/DocMsgManager.smali"
search = " invoke-virtual {v2, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V"
replace = " invoke-static {v2, v0}, Lrip/crit/ms/launcher/XiaomiProfile;->safeStart(Landroid/content/Context;Landroid/content/Intent;)V"
[[smaliPatches]]
targetFile = "com/note/launcher/MainActivity$1.smali"
search = " const-string v1, \"com.moan.notes\"\n\n invoke-virtual {v0, v1}, Landroid/content/pm/PackageManager;->getLaunchIntentForPackage(Ljava/lang/String;)Landroid/content/Intent;\n\n move-result-object v0\n\n .line 151\n .local v0, \"intent\":Landroid/content/Intent;\n iget-object v1, p0, Lcom/note/launcher/MainActivity$1;->this$0:Lcom/note/launcher/MainActivity;\n\n invoke-virtual {v1, v0}, Lcom/note/launcher/MainActivity;->startActivity(Landroid/content/Intent;)V"
replace = " invoke-static {v0}, Lrip/crit/ms/launcher/XiaomiProfile;->resolveNotesIntent(Landroid/content/pm/PackageManager;)Landroid/content/Intent;\n\n move-result-object v0\n\n .line 151\n .local v0, \"intent\":Landroid/content/Intent;\n if-eqz v0, :notes_missing\n\n iget-object v1, p0, Lcom/note/launcher/MainActivity$1;->this$0:Lcom/note/launcher/MainActivity;\n\n invoke-virtual {v1, v0}, Lcom/note/launcher/MainActivity;->startActivity(Landroid/content/Intent;)V\n\n goto :notes_launched\n\n :notes_missing\n iget-object v1, p0, Lcom/note/launcher/MainActivity$1;->this$0:Lcom/note/launcher/MainActivity;\n\n const-string v0, \"Notes app is not installed\"\n\n const/4 v2, 0x0\n\n invoke-static {v1, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;\n\n move-result-object v0\n\n invoke-virtual {v0}, Landroid/widget/Toast;->show()V\n\n :notes_launched"
[[smaliPatches]] [[smaliPatches]]
targetFile = "com/note/launcher/MainActivity.smali" targetFile = "com/note/launcher/MainActivity.smali"
search = " invoke-virtual {p0, v0}, Lcom/note/launcher/MainActivity;->setContentView(I)V\n\n .line 87" search = " invoke-virtual {p0, v0}, Lcom/note/launcher/MainActivity;->setContentView(I)V\n\n .line 87"
replace = " invoke-virtual {p0, v0}, Lcom/note/launcher/MainActivity;->setContentView(I)V\n\n invoke-static {p0}, Lrip/crit/ms/launcher/XiaomiProfile;->capture(Landroid/content/Context;)V\n\n .line 87" replace = " invoke-virtual {p0, v0}, Lcom/note/launcher/MainActivity;->setContentView(I)V\n\n invoke-static {p0}, Lrip/crit/ms/launcher/XiaomiProfile;->capture(Landroid/content/Context;)V\n\n .line 87"
[[smaliPatches]]
targetFile = "com/note/weight/NoteWeight.smali"
search = " invoke-static {}, Lcom/note/moannotelib/notes/summaries/NoteSummaries;->sGetInstance()Lcom/note/moannotelib/notes/summaries/NoteSummaries;\n\n move-result-object v0\n\n invoke-virtual {v0}, Lcom/note/moannotelib/notes/summaries/NoteSummaries;->updateSummaries()I"
replace = " invoke-static {}, Lcom/note/moannotelib/notes/summaries/NoteSummaries;->sGetInstance()Lcom/note/moannotelib/notes/summaries/NoteSummaries;\n\n move-result-object v0\n\n if-eqz v0, :empty_summaries\n\n invoke-virtual {v0}, Lcom/note/moannotelib/notes/summaries/NoteSummaries;->updateSummaries()I"
[[smaliPatches]]
targetFile = "com/note/weight/NoteWeight.smali"
search = " .line 210\n return-void\n.end method"
replace = " .line 210\n return-void\n\n :empty_summaries\n return-void\n.end method"

View file

@ -13,20 +13,9 @@ target = "en"
[generatedSmali] [generatedSmali]
sourceDir = "moannotes-native/build/smali/rip/crit/ms/moannotes/nativebridge" sourceDir = "moannotes-native/build/smali/rip/crit/ms/moannotes/nativebridge"
[[resources.xmlPatches]] # NOTE: the stock toolbar icons and menus are intentionally left untouched.
targetFile = "res/layout/note_edit.xml" # All patch UI lives outside it: one external FAB on the window decor opens
search = "android:src=\"@drawable/ic_noteview_stylus_ballpen\"" # the ToolsSheet bottom docker (layers / ink / snap / KRA export).
replace = "android:src=\"@drawable/icp_pen\" android:contentDescription=\"Pen\""
[[resources.xmlPatches]]
targetFile = "res/layout/note_edit.xml"
search = "android:src=\"@drawable/ic_noteview_background\""
replace = "android:src=\"@drawable/icp_palette\" android:contentDescription=\"Paper and monochrome ink\""
[[resources.xmlPatches]]
targetFile = "res/layout/note_edit.xml"
search = "android:src=\"@drawable/ic_noteview_more\""
replace = "android:src=\"@drawable/icp_export\" android:contentDescription=\"Document actions and export\""
[[smaliPatches]] [[smaliPatches]]
targetFile = "com/moan/notes/LaunchActivity.smali" targetFile = "com/moan/notes/LaunchActivity.smali"
@ -59,7 +48,23 @@ targetFile = "h/f/b/m5/j.smali"
search = " iget p2, p2, Lh/f/b/m5/l;->c:I\n\n invoke-virtual {p1, p2}, Landroid/widget/ImageView;->setImageResource(I)V" search = " iget p2, p2, Lh/f/b/m5/l;->c:I\n\n invoke-virtual {p1, p2}, Landroid/widget/ImageView;->setImageResource(I)V"
replace = " iget-object v1, p2, Lh/f/b/m5/l;->d:Ljava/lang/String;\n\n iget p2, p2, Lh/f/b/m5/l;->c:I\n\n iget-object v0, p0, Lh/f/b/m5/j;->h:Landroid/content/Context;\n\n invoke-static {v0, v1, p2}, Lrip/crit/ms/moannotes/BackgroundCatalogPatch;->previewResource(Landroid/content/Context;Ljava/lang/String;I)I\n\n move-result p2\n\n invoke-virtual {p1, p2}, Landroid/widget/ImageView;->setImageResource(I)V" replace = " iget-object v1, p2, Lh/f/b/m5/l;->d:Ljava/lang/String;\n\n iget p2, p2, Lh/f/b/m5/l;->c:I\n\n iget-object v0, p0, Lh/f/b/m5/j;->h:Landroid/content/Context;\n\n invoke-static {v0, v1, p2}, Lrip/crit/ms/moannotes/BackgroundCatalogPatch;->previewResource(Landroid/content/Context;Ljava/lang/String;I)I\n\n move-result p2\n\n invoke-virtual {p1, p2}, Landroid/widget/ImageView;->setImageResource(I)V"
# The stock page loader clamps a negative mEditLayerID but never an
# over-large one, so any layer-count shrink (delete) crashes note open with
# IndexOutOfBounds in u5/b/j. Upper-clamp here: corrupted notes open again.
[[smaliPatches]]
targetFile = "h/f/b/u5/b/j.smali"
search = " invoke-static {v2, v1}, Lcom/moan/notes/hand/data/config/LayerConfigData;->sSaveLayerConfigData(Landroid/content/Context;Lcom/moan/notes/hand/data/config/LayerConfigData;)I\n\n :cond_1e\n iget-object v1, v0, Lh/f/b/u5/a/b;->a:Ljava/util/ArrayList;"
replace = " invoke-static {v2, v1}, Lcom/moan/notes/hand/data/config/LayerConfigData;->sSaveLayerConfigData(Landroid/content/Context;Lcom/moan/notes/hand/data/config/LayerConfigData;)I\n\n :cond_1e\n iget-object v1, v0, Lh/f/b/u5/a/b;->a:Ljava/util/ArrayList;\n\n invoke-virtual {v1}, Ljava/util/ArrayList;->size()I\n\n move-result v1\n\n iget-object v2, v0, Lh/f/b/u5/a/b;->i:Lcom/moan/notes/hand/data/config/LayerConfigData;\n\n iget v2, v2, Lcom/moan/notes/hand/data/config/LayerConfigData;->mEditLayerID:I\n\n if-ge v2, v1, :layer_clamp_ok\n\n add-int/lit8 v1, v1, -0x1\n\n if-gez v1, :layer_clamp_store\n\n const/4 v1, 0x0\n\n :layer_clamp_store\n iget-object v2, v0, Lh/f/b/u5/a/b;->i:Lcom/moan/notes/hand/data/config/LayerConfigData;\n\n iput v1, v2, Lcom/moan/notes/hand/data/config/LayerConfigData;->mEditLayerID:I\n\n move v2, v1\n\n :layer_clamp_ok\n iget-object v1, v0, Lh/f/b/u5/a/b;->a:Ljava/util/ArrayList;"
[[smaliPatches]] [[smaliPatches]]
targetFile = "com/moan/notes/NoteActivity.smali" targetFile = "com/moan/notes/NoteActivity.smali"
search = " invoke-virtual {p1, v0}, Lcom/moan/notes/hand/view/HandwriteView;->setObserver(Lcom/moan/notes/hand/view/HWViewStatus$IStatusReporter;)V\n\n :cond_0\n return-void" search = " invoke-virtual {p1, v0}, Lcom/moan/notes/hand/view/HandwriteView;->setObserver(Lcom/moan/notes/hand/view/HWViewStatus$IStatusReporter;)V\n\n :cond_0\n return-void"
replace = " invoke-virtual {p1, v0}, Lcom/moan/notes/hand/view/HandwriteView;->setObserver(Lcom/moan/notes/hand/view/HWViewStatus$IStatusReporter;)V\n\n invoke-static {p0}, Lrip/crit/ms/moannotes/FeatureRegistry;->installNote(Landroid/app/Activity;)V\n\n :cond_0\n return-void" replace = " invoke-virtual {p1, v0}, Lcom/moan/notes/hand/view/HandwriteView;->setObserver(Lcom/moan/notes/hand/view/HWViewStatus$IStatusReporter;)V\n\n invoke-static {p0}, Lrip/crit/ms/moannotes/FeatureRegistry;->installNote(Landroid/app/Activity;)V\n\n :cond_0\n return-void"
# Rotation goes through onConfigurationChanged -> setContentView, which swaps
# the HandwriteView without rerunning init. Reinstall there so the snap
# listener follows the new view (FAB lives on the decor and survives).
[[smaliPatches]]
targetFile = "com/moan/notes/NoteActivity.smali"
search = " invoke-virtual {p0}, Lcom/moan/notes/NoteActivity;->D()V\n\n invoke-virtual {p0}, Lcom/moan/notes/NoteActivity;->R()V"
replace = " invoke-virtual {p0}, Lcom/moan/notes/NoteActivity;->D()V\n\n invoke-virtual {p0}, Lcom/moan/notes/NoteActivity;->R()V\n\n invoke-static {p0}, Lrip/crit/ms/moannotes/FeatureRegistry;->installNote(Landroid/app/Activity;)V"

View file

@ -45,6 +45,79 @@ export interface PatchConfig {
} }
const cjk = /[\u3400-\u9fff]/; const cjk = /[\u3400-\u9fff]/;
const preferredTranslations: Readonly<Record<string, string>> = {
" 搜索...": "Search",
按修改时间: "Modified",
按名称: "Name",
"背景:": "Background",
全部: "All",
书架: "Library",
关闭: "Close",
取消: "Cancel",
"加载中...": "Loading",
去设置WLAN: "Wi-Fi settings",
同步: "Sync",
删除: "Delete",
更新: "Update",
清空: "Clear",
测试: "Test",
标题: "Title",
搜索历史: "Search history",
"搜索...": "Search",
暂无数据: "No items",
暂无小部件: "No widgets",
暂无文件: "No files",
暂无搜索结果: "No results",
没有网络: "No network",
新增首页小部件: "Add home widget",
编辑首页小部件: "Edit home widget",
添加小部件: "Add widget",
点击添加小部件: "Add widget",
点击新建笔记: "New note",
笔记: "Notes",
笔记名称: "Note name",
笔记更新: "Notes update",
修改名称: "Rename",
重命名: "Rename",
知道了: "Done",
所有笔记加密密码相同: "Use one password for all notes",
关闭密码: "Disable password",
继续: "Continue",
"请输入4~8位数字": "Enter 4 to 8 digits",
"共 8 项": "8 items",
"录音文件名称录...": "Recording name",
"请按名称或主题关键词搜索书籍,笔记": "Search books and notes",
支持搜索多看的在线书籍: "Search online books",
请输入书名或作者名称关键词: "Search by title or author",
阅读数据: "Reading data",
首页: "Home",
优化配置: "Optimize",
应用: "Apps",
卸载: "Uninstall",
请使用手机扫描查看: "Scan with your phone",
阅读分享: "Share reading",
打开多看: "Open Duokan",
"将PDF文档放至Books或者Download文件夹内\n就能在此显示哦~":
"Put PDF files in Books or Download to see them here",
最近浏览: "Recent",
本地PDF: "Local PDFs",
多看: "Duokan",
今日阅读: "Reading today",
全网排行: "Rankings",
全网阅读数据: "Reading statistics",
分钟: "minutes",
名: "rank",
"点击登录系统账号,开始统计": "Sign in to track reading",
目标完成: "Goal reached",
近七天阅读: "Last 7 days",
查看最近文档: "Recent documents",
"作者:": "Author",
加入书架: "Add to library",
已在书架: "In library",
文件夹: "Folder",
"移动到…./文件夹1/文件夹2/文件夹3/文件夹4/文件夹5/文件夹6/文件夹7/文件夹8/文件夹9/文件夹10":
"Move to folder",
};
const signaturePermissions = new Set([ const signaturePermissions = new Set([
"android.permission.CHANGE_CONFIGURATION", "android.permission.CHANGE_CONFIGURATION",
"android.permission.DELETE_PACKAGES", "android.permission.DELETE_PACKAGES",
@ -189,10 +262,22 @@ function renameManifestPackage(
const match = xml.match(/\bpackage="([^"]+)"/); const match = xml.match(/\bpackage="([^"]+)"/);
if (!match) throw new Error("Manifest package attribute was not found"); if (!match) throw new Error("Manifest package attribute was not found");
const original = match[1]!; const original = match[1]!;
const renamed = original.startsWith(`${prefix}.`) // A prefix identical to (or already containing) the original package
? original // means "keep the stock package": the tablet breaks when the installer
: `${prefix}.${original}`; // namespace changes, so never double-prefix (com.moan.notes must stay
// com.moan.notes).
const renamed =
original === prefix || original.startsWith(`${prefix}.`)
? original
: `${prefix}.${original}`;
xml = xml.replace(`package="${original}"`, `package="${renamed}"`); xml = xml.replace(`package="${original}"`, `package="${renamed}"`);
// Provider authorities belong to the installation namespace, not the
// signing namespace. A renamed package must always receive a renamed
// authority or Android rejects it while the OEM package is still present.
xml = xml.replaceAll(
`android:authorities="${original}`,
`android:authorities="${renamed}`,
);
if (ordinarySignature) { if (ordinarySignature) {
xml = xml.replace(/\s+android:sharedUserId="[^"]+"/, ""); xml = xml.replace(/\s+android:sharedUserId="[^"]+"/, "");
xml = xml.replace( xml = xml.replace(
@ -200,10 +285,6 @@ function renameManifestPackage(
(element, permission: string) => (element, permission: string) =>
signaturePermissions.has(permission) ? "" : element, signaturePermissions.has(permission) ? "" : element,
); );
xml = xml.replaceAll(
`android:authorities="${original}`,
`android:authorities="${renamed}`,
);
console.log(" -> Removed shared system UID and signature-only permissions"); console.log(" -> Removed shared system UID and signature-only permissions");
} }
fs.writeFileSync(manifestPath, xml); fs.writeFileSync(manifestPath, xml);
@ -239,10 +320,49 @@ function createTranslatedStringOverlay(
value: match[3]!, value: match[3]!,
}); });
} }
if (!entries.length) return; if (entries.length) {
const translations = translateTexts(
entries.map((entry) => entry.value),
config,
projectRoot,
);
const strings = entries.map(
(entry, index) =>
` <string name="${entry.name}"${entry.attributes}>${escapeXmlText(translations[index]!)}</string>`,
);
const target = config.target ?? "en";
const destination = path.join(
workDir,
"res",
`values-${target}`,
"strings.xml",
);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(
destination,
`<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${strings.join("\n")}\n</resources>\n`,
);
console.log(
` -> Added ${strings.length} ${target} translations through ${config.backend ?? "argos"}`,
);
}
createTranslatedLayoutOverlays(workDir, config, projectRoot);
}
function translateTexts(
texts: string[],
config: TranslationConfig,
projectRoot: string,
): string[] {
if (!texts.length) return [];
const translations = texts.map((text) => preferredTranslations[text]);
const unresolved = texts
.map((text, index) => ({ text, index }))
.filter(({ index }) => translations[index] === undefined);
if (!unresolved.length) return translations as string[];
const apiKey = config.apiKeyEnv ? process.env[config.apiKeyEnv] : undefined; const apiKey = config.apiKeyEnv ? process.env[config.apiKeyEnv] : undefined;
const request = JSON.stringify({ const request = JSON.stringify({
texts: entries.map((entry) => entry.value), texts: unresolved.map(({ text }) => text),
backend: config.backend ?? "argos", backend: config.backend ?? "argos",
source: config.source ?? "zh", source: config.source ?? "zh",
target: config.target ?? "en", target: config.target ?? "en",
@ -278,28 +398,73 @@ function createTranslatedStringOverlay(
const response = JSON.parse(result.stdout) as { translations: string[] }; const response = JSON.parse(result.stdout) as { translations: string[] };
if ( if (
!Array.isArray(response.translations) || !Array.isArray(response.translations) ||
response.translations.length !== entries.length response.translations.length !== unresolved.length
) )
throw new Error("Translation bridge returned an invalid result count"); throw new Error("Translation bridge returned an invalid result count");
const strings = entries.map( for (const [index, translation] of response.translations.entries())
(entry, index) => translations[unresolved[index]!.index] = translation;
` <string name="${entry.name}"${entry.attributes}>${escapeXmlText(response.translations[index]!)}</string>`, return translations as string[];
); }
function createTranslatedLayoutOverlays(
workDir: string,
config: TranslationConfig,
projectRoot: string,
): void {
const resourceRoot = path.join(workDir, "res");
const target = config.target ?? "en"; const target = config.target ?? "en";
const destination = path.join( const attributePattern =
workDir, /\bandroid:(text|hint|contentDescription)="([^"]*)"/g;
"res", const records: Array<{
`values-${target}`, destination: string;
"strings.xml", content: string;
); values: string[];
fs.mkdirSync(path.dirname(destination), { recursive: true }); }> = [];
fs.writeFileSync( for (const directory of fs
destination, .readdirSync(resourceRoot)
`<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${strings.join("\n")}\n</resources>\n`, .filter((name) => /^layout(?:-.+)?$/.test(name))) {
); const sourceDirectory = path.join(resourceRoot, directory);
console.log( for (const file of fs
` -> Added ${strings.length} ${target} translations through ${config.backend ?? "argos"}`, .readdirSync(sourceDirectory)
.filter((name) => name.endsWith(".xml"))) {
const source = path.join(sourceDirectory, file);
const content = fs.readFileSync(source, "utf8");
const values = Array.from(content.matchAll(attributePattern))
.map((match) => match[2]!)
.filter((value) => cjk.test(value));
if (!values.length) continue;
const qualifier = directory.slice("layout".length);
records.push({
destination: path.join(
resourceRoot,
`layout-${target}${qualifier}`,
file,
),
content,
values,
});
}
}
if (!records.length) return;
const translations = translateTexts(
records.flatMap((record) => record.values),
config,
projectRoot,
); );
let translationIndex = 0;
for (const record of records) {
const translated = record.content.replace(
attributePattern,
(match, attribute: string, value: string) => {
if (!cjk.test(value)) return match;
const text = translations[translationIndex++]!;
return `android:${attribute}="${escapeXmlAttribute(text)}"`;
},
);
fs.mkdirSync(path.dirname(record.destination), { recursive: true });
fs.writeFileSync(record.destination, translated);
}
console.log(` -> Added ${translationIndex} ${target} layout translations`);
} }
function escapeXmlText(value: string): string { function escapeXmlText(value: string): string {
@ -309,6 +474,10 @@ function escapeXmlText(value: string): string {
.replace(/'/g, "\\'"); .replace(/'/g, "\\'");
} }
function escapeXmlAttribute(value: string): string {
return escapeXmlText(value).replaceAll('"', "&quot;");
}
function applySmaliPatches( function applySmaliPatches(
workDir: string, workDir: string,
patches: SmaliPatch[], patches: SmaliPatch[],

View file

@ -1,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
git ls-files -co --exclude-standard | grep '\.java$' | xargs java \
git ls-files -co --exclude-standard | grep '\.java$' | sed 's/ /\\ /g' | xargs java \
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
-jar google-java-format-1.37.0-all-deps.jar --replace -jar google-java-format-1.37.0-all-deps.jar --replace

View file

@ -0,0 +1,141 @@
package rip.crit.ms.moannotes.nativebridge;
import android.app.Activity;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.view.Gravity;
import android.view.View;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.lang.reflect.Field;
/**
* Ink docker: monochrome pen selector (white plus named greys through black) with a persistent
* default. The choice is written straight into {@code HandwriteView.mColorARGB} via reflection, so
* new strokes use it immediately, and re-applied on every editor install so reopening the note
* keeps the ink.
*/
public final class InkPanel {
private static final String KEY = "ink_argb";
private static final int[] INKS = {
0xFFFFFFFF, 0xFFD9D9D9, 0xFFB3B3B3, 0xFF808080, 0xFF595959, 0xFF333333, 0xFF000000,
};
private static final String[] NAMES = {
"White", "Silver", "Grey", "Mid grey", "Dark grey", "Charcoal", "Black",
};
private InkPanel() {}
public static void show(Activity activity) {
Object canvas = NativeFeatureHooks.findCanvas(activity);
int current = saved(activity);
if (canvas != null) {
Integer live = readColor(canvas);
if (live != null) current = live;
}
final int selected = current;
LinearLayout body = new LinearLayout(activity);
body.setOrientation(LinearLayout.VERTICAL);
TextView hint = new TextView(activity);
hint.setText("Monochrome ink");
hint.setTextColor(0xFF616161);
hint.setTextSize(13);
body.addView(hint);
GridLayout grid = new GridLayout(activity);
grid.setColumnCount(4);
grid.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
int gap = PanelDocker.dp(activity, 10);
grid.setPadding(0, gap, 0, gap);
for (int i = 0; i < INKS.length; i++) {
final int color = INKS[i];
View swatch = new View(activity);
int size = PanelDocker.dp(activity, 44);
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.width = size;
params.height = size;
params.setMargins(gap, gap, gap, gap);
params.setGravity(Gravity.CENTER);
swatch.setLayoutParams(params);
swatch.setContentDescription(NAMES[i] + " ink");
swatch.setBackground(ring(color, color == selected));
swatch.setOnClickListener(
v -> {
apply(activity, NativeFeatureHooks.findCanvas(activity), color);
show(activity);
});
grid.addView(swatch);
}
body.addView(grid);
TextView name = new TextView(activity);
name.setText("Current: " + nameOf(selected));
name.setTextColor(Color.BLACK);
name.setTextSize(15);
body.addView(name);
PanelDocker.show(activity, "Ink", body);
}
/** Re-applies the saved ink after the editor (re)creates its canvas. */
static void applySaved(Activity activity, Object canvas) {
if (canvas == null) return;
Integer live = readColor(canvas);
int saved = saved(activity);
if (live != null && live == saved) return;
writeColor(canvas, saved);
}
private static void apply(Activity activity, Object canvas, int color) {
saved(activity, color);
if (canvas != null) writeColor(canvas, color);
}
private static GradientDrawable ring(int fill, boolean selected) {
GradientDrawable drawable = new GradientDrawable();
drawable.setShape(GradientDrawable.OVAL);
drawable.setColor(fill);
drawable.setStroke(selected ? 6 : 2, selected ? 0xFF1A1A1A : 0xFF9E9E9E);
return drawable;
}
private static Integer readColor(Object canvas) {
try {
Field field = canvas.getClass().getDeclaredField("mColorARGB");
field.setAccessible(true);
return (Integer) field.get(canvas);
} catch (Exception ignored) {
return null;
}
}
private static void writeColor(Object canvas, int color) {
try {
Field field = canvas.getClass().getDeclaredField("mColorARGB");
field.setAccessible(true);
field.setInt(canvas, color);
} catch (Exception ignored) {
}
}
private static int saved(Activity activity) {
return prefs(activity).getInt(KEY, 0xFF000000);
}
private static void saved(Activity activity, int color) {
prefs(activity).edit().putInt(KEY, color).apply();
}
private static SharedPreferences prefs(Activity activity) {
return activity.getSharedPreferences(NativeFeatureHooks.PREFS, 0);
}
private static String nameOf(int color) {
for (int i = 0; i < INKS.length; i++) if (INKS[i] == color) return NAMES[i];
return String.format("#%06X", color & 0xFFFFFF);
}
}

View file

@ -2,154 +2,279 @@ package rip.crit.ms.moannotes.nativebridge;
import android.app.Activity; import android.app.Activity;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
/**
* Layers docker, wired to the engine's real layer model.
*
* <p>How the stock renderer works (verified in smali): the live page object ({@code Lh/f/b/u5/a/b})
* owns the layer runtimes in list {@code a} and the draw target in {@code c}; {@code dispatchDraw}
* composites every entry of {@code a} unconditionally. There is no per-layer visibility channel, so
* a hide/show toggle cannot exist — it is intentionally absent here. What the engine honors, and
* what this panel does:
*
* <ul>
* <li>Active layer: {@code mEditLayerID} plus the live pointer {@code c = a.get(id)}, saved and
* refreshed. New strokes attach to that layer.
* <li>Add / delete: config op on the live object, saved, then the current page is reloaded
* through the stock queue ({@code saveCurPage} first so no stroke is lost), which rebuilds
* the runtime list from disk.
* </ul>
*/
public final class LayerPanel { public final class LayerPanel {
private LayerPanel() {} private LayerPanel() {}
public static void show(Activity activity) { public static void show(Activity activity) {
try { try {
Object canvas = NativeFeatureHooks.findCanvas(activity); Object canvas = NativeFeatureHooks.findCanvas(activity);
if (canvas == null) return; if (canvas == null) {
Object page = publicField(canvas, "mPageConfigData"); warn("show: no canvas");
int pageId = ((Integer) publicField(page, "mEditPage")).intValue(); return;
}
Class<?> layerClass = Class.forName("com.moan.notes.hand.data.config.LayerConfigData"); Class<?> layerClass = Class.forName("com.moan.notes.hand.data.config.LayerConfigData");
Object config = // Operate on the renderer's LIVE config (mCurPage.i). Mutating a
layerClass // separately loaded copy only reached disk, so edits never showed.
.getMethod( Object config = liveConfig(canvas);
"sLoadLayerConfig", if (config == null) config = loadFromDisk(activity, canvas, layerClass);
android.content.Context.class, if (config == null) {
page.getClass(), warn("show: no layer config");
int.class, return;
layerClass) }
.invoke(null, activity, page, pageId, null); PanelDocker.show(activity, "Layers", body(activity, canvas, config, layerClass));
if (config == null) return; } catch (Exception error) {
popup(activity, canvas, config, layerClass); warn("show: " + error);
}
}
private static void warn(String message) {
try {
android.util.Log.w("MiStringLayers", message);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
private static void popup(Activity activity, Object canvas, Object config, Class<?> layerClass) /** Returns the live {@code LayerConfigData} the renderer draws from, if loaded. */
private static Object liveConfig(Object canvas) {
try {
Object curPage = declaredField(canvas, "mCurPage");
if (curPage == null) return null;
return declaredField(curPage, "i");
} catch (Exception ignored) {
return null;
}
}
private static Object loadFromDisk(Activity activity, Object canvas, Class<?> layerClass)
throws Exception { throws Exception {
LinearLayout list = new LinearLayout(activity); Object page = field(canvas, "mPageConfigData");
list.setOrientation(LinearLayout.VERTICAL); if (page == null) return null;
list.setPadding(dp(activity, 16), dp(activity, 16), dp(activity, 16), dp(activity, 16)); int pageId = ((Integer) field(page, "mEditPage")).intValue();
list.setBackgroundColor(Color.WHITE); return layerClass
TextView title = new TextView(activity); .getMethod(
title.setText("Layers"); "sLoadLayerConfig",
title.setTextColor(Color.BLACK); android.content.Context.class,
title.setTextSize(20); page.getClass(),
list.addView(title); int.class,
layerClass)
.invoke(null, activity, page, pageId, null);
}
private static LinearLayout body(
Activity activity, Object canvas, Object config, Class<?> layerClass) throws Exception {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ArrayList<Integer> visible = (ArrayList<Integer>) publicField(config, "mEditLayerVisible"); ArrayList<?> folders = (ArrayList<?>) field(config, "mLayerDataFolders");
String parent = (String) publicField(config, "mParentPath"); int count = folders.size();
for (int index = visible.size() - 1; index >= 0; index--) { int active = ((Integer) field(config, "mEditLayerID")).intValue();
LinearLayout body = new LinearLayout(activity);
body.setOrientation(LinearLayout.VERTICAL);
TextView hint = new TextView(activity);
hint.setText("Tap a row to draw on that layer");
hint.setTextColor(0xFF616161);
hint.setTextSize(13);
body.addView(hint);
for (int index = count - 1; index >= 0; index--) {
final int layer = index; final int layer = index;
boolean isActive = layer == active;
LinearLayout row = new LinearLayout(activity); LinearLayout row = new LinearLayout(activity);
row.setGravity(Gravity.CENTER_VERTICAL); row.setGravity(Gravity.CENTER_VERTICAL);
Button eye = button(activity, visible.get(index) != 0 ? "Hide" : "Show"); int vPad = PanelDocker.dp(activity, 10);
eye.setOnClickListener( row.setPadding(0, vPad, 0, vPad);
v -> {
try { // Active-layer indicator dot.
visible.set(layer, visible.get(layer) == 0 ? 1 : 0); View dot = new View(activity);
save(activity, config, layerClass); int dotSize = PanelDocker.dp(activity, 10);
refresh(canvas); LinearLayout.LayoutParams dotParams = new LinearLayout.LayoutParams(dotSize, dotSize);
show(activity); dotParams.rightMargin = PanelDocker.dp(activity, 10);
} catch (Exception ignored) { dot.setLayoutParams(dotParams);
} android.graphics.drawable.GradientDrawable dotBg =
}); new android.graphics.drawable.GradientDrawable();
dotBg.setShape(android.graphics.drawable.GradientDrawable.OVAL);
dotBg.setColor(isActive ? 0xFF1A1A1A : 0x00000000);
dotBg.setStroke(PanelDocker.dp(activity, 1), 0xFF9E9E9E);
dot.setBackground(dotBg);
row.addView(dot);
TextView name = new TextView(activity); TextView name = new TextView(activity);
name.setText("Layer " + (index + 1)); name.setText("Layer " + (layer + 1) + (isActive ? " · active" : ""));
name.setTextColor(Color.BLACK); name.setTextColor(Color.BLACK);
name.setTextSize(16); name.setTextSize(16);
name.setPadding(dp(activity, 12), 0, dp(activity, 12), 0); row.addView(name, new LinearLayout.LayoutParams(0, -2, 1));
Button remove = button(activity, "Remove");
remove.setEnabled(visible.size() > 1); row.setOnClickListener(
remove.setOnClickListener(
v -> { v -> {
try { try {
layerClass selectActive(activity, canvas, config, layerClass, layer);
.getMethod(
"sDeleteLayerFiles", android.content.Context.class, layerClass, int.class)
.invoke(null, activity, config, layer);
refresh(canvas);
show(activity); show(activity);
} catch (Exception ignored) { } catch (Exception error) {
warn("row/add/delete: " + error);
} }
}); });
row.addView(eye); body.addView(row);
row.addView(name, new LinearLayout.LayoutParams(0, -2, 1)); if (index > 0) body.addView(PanelDocker.hairline(activity));
row.addView(remove);
list.addView(row);
SeekBar opacity = new SeekBar(activity);
opacity.setMax(255);
opacity.setProgress(LayerSettings.opacity(activity, parent, layer));
opacity.setContentDescription("Layer opacity");
opacity.setOnSeekBarChangeListener(
new SeekBar.OnSeekBarChangeListener() {
public void onProgressChanged(SeekBar bar, int value, boolean user) {
if (user) LayerSettings.opacity(activity, parent, layer, value);
}
public void onStartTrackingTouch(SeekBar bar) {}
public void onStopTrackingTouch(SeekBar bar) {}
});
list.addView(opacity);
Spinner blend = new Spinner(activity);
String[] modes = {"normal", "multiply", "screen", "overlay", "darken", "lighten"};
blend.setAdapter(
new ArrayAdapter<>(activity, android.R.layout.simple_spinner_dropdown_item, modes));
String current = LayerSettings.blend(activity, parent, layer);
for (int mode = 0; mode < modes.length; mode++)
if (modes[mode].equals(current)) blend.setSelection(mode);
blend.setOnItemSelectedListener(
new android.widget.AdapterView.OnItemSelectedListener() {
public void onItemSelected(
android.widget.AdapterView<?> p, View v, int position, long itemId) {
LayerSettings.blend(activity, parent, layer, modes[position]);
}
public void onNothingSelected(android.widget.AdapterView<?> p) {}
});
list.addView(blend);
} }
Button add = button(activity, "Add layer");
body.addView(PanelDocker.hairline(activity));
LinearLayout actions = new LinearLayout(activity);
actions.setOrientation(LinearLayout.VERTICAL);
int top = PanelDocker.dp(activity, 8);
actions.setPadding(0, top, 0, 0);
Button add = actionButton(activity, "Add layer");
add.setOnClickListener( add.setOnClickListener(
v -> { v -> {
try { try {
int count = ((ArrayList<?>) publicField(config, "mLayerDataFolders")).size(); Object live = liveConfig(NativeFeatureHooks.findCanvas(activity));
Object target = live != null ? live : config;
int size = ((ArrayList<?>) field(target, "mLayerDataFolders")).size();
savePage(canvas);
layerClass layerClass
.getMethod("sAddLayerFiles", android.content.Context.class, layerClass, int.class) .getMethod("sAddLayerFiles", android.content.Context.class, layerClass, int.class)
.invoke(null, activity, config, count); .invoke(null, activity, target, size);
refresh(canvas); save(activity, target, layerClass);
reloadPage(canvas);
show(activity); show(activity);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
}); });
list.addView(add); actions.addView(add);
ScrollView scroll = new ScrollView(activity);
scroll.addView(list); Button remove = actionButton(activity, "Delete active layer");
PopupWindow window = new PopupWindow(scroll, dp(activity, 300), -2, true); remove.setEnabled(count > 1 && active >= 0 && active < count);
window.setBackgroundDrawable(surface()); remove.setOnClickListener(
window.setElevation(dp(activity, 8)); v -> {
window.showAtLocation( try {
activity.getWindow().getDecorView(), Object live = liveConfig(NativeFeatureHooks.findCanvas(activity));
Gravity.RIGHT | Gravity.TOP, Object target = live != null ? live : config;
dp(activity, 12), savePage(canvas);
dp(activity, 64)); Object next =
layerClass
.getMethod(
"sDeleteLayerFiles", android.content.Context.class, layerClass, int.class)
.invoke(null, activity, target, active);
// sDeleteLayerFiles shrinks the lists but leaves mEditLayerID
// stale; an over-large id crashes note open in the page loader.
// Honor its returned index, clamped into range.
int size = ((ArrayList<?>) field(target, "mLayerDataFolders")).size();
int fixed = ((Integer) next).intValue();
if (fixed < 0) fixed = 0;
if (size > 0 && fixed > size - 1) fixed = size - 1;
layerClass.getField("mEditLayerID").set(target, fixed);
save(activity, target, layerClass);
reloadPage(canvas);
show(activity);
} catch (Exception ignored) {
}
});
actions.addView(remove);
Button export = actionButton(activity, "Export KRA");
export.setOnClickListener(
v -> {
PanelDocker.dismiss();
Object target = NativeFeatureHooks.findCanvas(activity);
if (target != null) KraExporter.exportCurrentPage(activity, target);
});
actions.addView(export);
body.addView(actions);
return body;
}
/**
* Switches the draw target: config id plus the live runtime pointer ({@code b.c = b.a.get(id)}),
* which is what new strokes actually follow. Setting the id alone left strokes landing on the
* previous layer.
*/
private static void selectActive(
Activity activity, Object canvas, Object config, Class<?> layerClass, int layer)
throws Exception {
Object page = livePage(canvas);
if (page != null) {
@SuppressWarnings("unchecked")
ArrayList<Object> runtimes = (ArrayList<Object>) field(page, "a");
if (layer >= 0 && layer < runtimes.size()) {
page.getClass().getField("c").set(page, runtimes.get(layer));
}
}
layerClass.getField("mEditLayerID").set(config, layer);
save(activity, config, layerClass);
refresh(canvas);
}
/**
* Reloads the current page through the stock queue (same sequence as page turn) so the runtime
* layer list is rebuilt from disk after add/delete.
*/
private static void reloadPage(Object canvas) throws Exception {
Object queue = field(canvas, "queueManager");
int pageIdx = ((Integer) canvas.getClass().getMethod("getPageIdx").invoke(canvas)).intValue();
canvas
.getClass()
.getMethod("addLoadPageTask", queue.getClass(), int.class)
.invoke(canvas, queue, pageIdx);
canvas
.getClass()
.getMethod("addReportTask", queue.getClass(), int.class, int.class)
.invoke(canvas, queue, 0, 1);
Method kick = null;
for (Method method : queue.getClass().getMethods()) {
if (method.getName().equals("d") && method.getParameterTypes().length == 2) kick = method;
}
if (kick != null) kick.invoke(queue, false, false);
refresh(canvas);
}
private static Object livePage(Object canvas) {
try {
return declaredField(canvas, "mCurPage");
} catch (Exception ignored) {
return null;
}
}
private static void savePage(Object canvas) {
try {
canvas.getClass().getMethod("saveCurPage").invoke(canvas);
} catch (Exception ignored) {
}
}
private static Button actionButton(Activity activity, String label) {
Button button = new Button(activity);
button.setText(label);
button.setTextColor(Color.BLACK);
button.setAllCaps(false);
return button;
} }
private static void save(Activity activity, Object config, Class<?> layerClass) throws Exception { private static void save(Activity activity, Object config, Class<?> layerClass) throws Exception {
@ -162,27 +287,14 @@ public final class LayerPanel {
canvas.getClass().getMethod("refreshUILayer").invoke(canvas); canvas.getClass().getMethod("refreshUILayer").invoke(canvas);
} }
private static Object publicField(Object o, String name) throws Exception { private static Object field(Object object, String name) throws Exception {
Field f = o.getClass().getField(name); Field field = object.getClass().getField(name);
return f.get(o); return field.get(object);
} }
private static Button button(Activity a, String label) { private static Object declaredField(Object object, String name) throws Exception {
Button b = new Button(a); Field field = object.getClass().getDeclaredField(name);
b.setText(label); field.setAccessible(true);
b.setTextColor(Color.BLACK); return field.get(object);
b.setAllCaps(false);
return b;
}
private static int dp(Activity a, int value) {
return Math.round(value * a.getResources().getDisplayMetrics().density);
}
private static GradientDrawable surface() {
GradientDrawable g = new GradientDrawable();
g.setColor(Color.WHITE);
g.setCornerRadius(24);
return g;
} }
} }

View file

@ -1,72 +1,158 @@
package rip.crit.ms.moannotes.nativebridge; package rip.crit.ms.moannotes.nativebridge;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* Single integration point for the note-editor patches.
*
* <p>The stock toolbar is never touched: no buttons are added, no icons are replaced. All patch UI
* lives in one external floating action button anchored to the window decor (bottom-end, clear of
* the page bar), which opens the {@link ToolsSheet} bottom docker. Native editor UI (toolbar,
* menus, background picker) keeps working exactly as shipped.
*
* <p>The smali-side snap listener is touched through reflection only, so this module still compiles
* without the injected patch classes on the classpath.
*/
public final class NativeFeatureHooks { public final class NativeFeatureHooks {
static final String PREFS = "moannotes_patch";
static final String SNAP_KEY = "snap_to_line_enabled";
private static final String LISTENER_CLASS = "rip.crit.ms.moannotes.SnapToLineTouchListener";
private NativeFeatureHooks() {} private NativeFeatureHooks() {}
public static void install(Activity activity) { public static void install(Activity activity) {
LinearLayout bar = findBar(activity); View handwrite = findHandwriteView(activity);
if (bar == null || activity.findViewById(id(activity, "moannotes_kra_export_button")) != null) if (handwrite == null) return;
return; ensureSnapListener(activity, handwrite);
add( ensureFab(activity);
activity, InkPanel.applySaved(activity, findCanvas(activity));
bar,
"moannotes_layers_button",
"icp_layers",
"Layers",
v -> LayerPanel.show(activity));
add(
activity,
bar,
"moannotes_kra_export_button",
"icp_kra_export",
"Export KRA",
v -> {
Object canvas = findCanvas(activity);
if (canvas != null) KraExporter.exportCurrentPage(activity, canvas);
});
} }
static Object findCanvas(Activity activity) { // ---- external floating entry point (decor only, never the toolbar) ------
private static void ensureFab(Activity activity) {
int fabId = PanelDocker.id(activity, "moannotes_tools_fab");
View root = activity.getWindow().getDecorView();
if (!(root instanceof ViewGroup)) return;
if (fabId != 0 && activity.findViewById(fabId) != null) return;
ImageButton fab = new ImageButton(activity);
if (fabId != 0) fab.setId(fabId);
fab.setContentDescription("Patch tools");
int icon = PanelDocker.id(activity, "icp_tools");
if (icon != 0) fab.setImageResource(icon);
fab.setScaleType(ImageButton.ScaleType.FIT_CENTER);
fab.setBackground(fabFace());
int size = PanelDocker.dp(activity, 56);
int pad = PanelDocker.dp(activity, 14);
fab.setPadding(pad, pad, pad, pad);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
params.gravity = Gravity.BOTTOM | Gravity.END;
params.rightMargin = PanelDocker.dp(activity, 16);
params.bottomMargin = PanelDocker.dp(activity, 88);
fab.setLayoutParams(params);
if (android.os.Build.VERSION.SDK_INT >= 21) fab.setElevation(PanelDocker.dp(activity, 6));
fab.setOnClickListener(v -> ToolsSheet.show(activity));
((ViewGroup) root).addView(fab);
}
private static GradientDrawable fabFace() {
GradientDrawable drawable = new GradientDrawable();
drawable.setShape(GradientDrawable.OVAL);
drawable.setColor(Color.WHITE);
drawable.setStroke(2, 0xFFE0E0E0);
return drawable;
}
// ---- snap-to-line (reflection: class lives in injected smali) ------------
private static void ensureSnapListener(Activity activity, View handwrite) {
if (taggedListener(activity, handwrite) != null) return;
try { try {
return activity.getClass().getField("g").get(activity); Class<?> type = Class.forName(LISTENER_CLASS);
Constructor<?> init = type.getConstructor(Context.class, boolean.class);
Object listener = init.newInstance(activity, prefs(activity).getBoolean(SNAP_KEY, false));
handwrite.setOnTouchListener((View.OnTouchListener) listener);
int tagId = snapTagId(activity);
if (tagId != 0) handwrite.setTag(tagId, listener);
} catch (Exception ignored) { } catch (Exception ignored) {
return null;
} }
} }
private static LinearLayout findBar(Activity activity) { static Object snapListener(Activity activity, View handwrite) {
View view = activity.findViewById(id(activity, "edit_button_bar")); Object listener = taggedListener(activity, handwrite);
return view instanceof LinearLayout ? (LinearLayout) view : null; if (listener != null) return listener;
ensureSnapListener(activity, handwrite);
return taggedListener(activity, handwrite);
} }
private static void add( private static Object taggedListener(Activity activity, View handwrite) {
Activity a, int tagId = snapTagId(activity);
LinearLayout bar, if (tagId == 0) return null;
String idName, try {
String drawable, Object tag = handwrite.getTag(tagId);
String description, if (tag != null && tag.getClass().getName().equals(LISTENER_CLASS)) return tag;
View.OnClickListener action) { } catch (Exception ignored) {
ImageButton button = new ImageButton(a); }
button.setId(id(a, idName)); return null;
button.setContentDescription(description);
button.setBackgroundColor(Color.TRANSPARENT);
button.setImageResource(id(a, drawable));
int size = Math.round(48 * a.getResources().getDisplayMetrics().density);
button.setLayoutParams(new LinearLayout.LayoutParams(size, -1));
button.setOnClickListener(action);
bar.addView(button);
} }
static int id(Activity activity, String name) { private static int snapTagId(Activity activity) {
return activity return PanelDocker.id(activity, "moannotes_tools_fab");
.getResources() }
.getIdentifier(
name, name.startsWith("icp_") ? "drawable" : "id", activity.getPackageName()); static boolean isSnapEnabled(Object listener) {
try {
Method isEnabled = listener.getClass().getMethod("isEnabled");
return Boolean.TRUE.equals(isEnabled.invoke(listener));
} catch (Exception ignored) {
return false;
}
}
static void setSnapEnabled(Activity activity, Object listener, boolean enabled) {
try {
Method setEnabled = listener.getClass().getMethod("setEnabled", boolean.class);
setEnabled.invoke(listener, enabled);
} catch (Exception ignored) {
return;
}
prefs(activity).edit().putBoolean(SNAP_KEY, enabled).apply();
}
static boolean snapEnabled(Activity activity) {
return prefs(activity).getBoolean(SNAP_KEY, false);
}
// ---- shared lookups ------------------------------------------------------
static Object findCanvas(Activity activity) {
// NoteActivity.g is the public HandwriteView field.
try {
Field field = activity.getClass().getField("g");
Object canvas = field.get(activity);
if (canvas != null) return canvas;
} catch (Exception ignored) {
}
return findHandwriteView(activity);
}
static View findHandwriteView(Activity activity) {
return activity.findViewById(PanelDocker.id(activity, "note_handwrite_view"));
}
static SharedPreferences prefs(Activity activity) {
return activity.getSharedPreferences(PREFS, 0);
} }
} }

View file

@ -0,0 +1,122 @@
package rip.crit.ms.moannotes.nativebridge;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.ScrollView;
import android.widget.TextView;
/**
* Shared chrome for all patch floaters (layers + ink). One visible panel at a time, same rounded
* surface everywhere so the patches read as one UI.
*/
public final class PanelDocker {
private static PopupWindow current;
private PanelDocker() {}
/** Shows {@code content} in the shared rounded floater, replacing any open panel. */
public static void show(Activity activity, String title, View content) {
dismiss();
LinearLayout card = new LinearLayout(activity);
card.setOrientation(LinearLayout.VERTICAL);
card.setBackground(surface());
int pad = dp(activity, 16);
card.setPadding(pad, dp(activity, 12), pad, pad);
if (android.os.Build.VERSION.SDK_INT >= 21) card.setElevation(dp(activity, 8));
LinearLayout header = new LinearLayout(activity);
header.setGravity(Gravity.CENTER_VERTICAL);
TextView label = new TextView(activity);
label.setText(title);
label.setTextColor(Color.BLACK);
label.setTextSize(18);
LinearLayout.LayoutParams labelParams =
new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1);
header.addView(label, labelParams);
ImageButton close = new ImageButton(activity);
close.setContentDescription("Close panel");
close.setBackgroundColor(Color.TRANSPARENT);
close.setImageResource(id(activity, "icp_close"));
int touch = dp(activity, 44);
close.setLayoutParams(new LinearLayout.LayoutParams(touch, touch));
close.setOnClickListener(v -> dismiss());
header.addView(close);
card.addView(header);
View divider = new View(activity);
divider.setBackgroundColor(0xFFE0E0E0);
LinearLayout.LayoutParams dividerParams =
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dp(activity, 1));
dividerParams.topMargin = dp(activity, 8);
dividerParams.bottomMargin = dp(activity, 8);
card.addView(divider, dividerParams);
ScrollView scroll = new ScrollView(activity);
scroll.addView(content);
card.addView(scroll);
PopupWindow window = new PopupWindow(card, dp(activity, 300), dp(activity, 420), true);
window.setBackgroundDrawable(surface());
window.setOnDismissListener(() -> current = null);
current = window;
window.showAtLocation(
activity.getWindow().getDecorView(),
Gravity.END | Gravity.TOP,
dp(activity, 12),
dp(activity, 72));
}
public static void dismiss() {
if (current != null) {
try {
current.dismiss();
} catch (Exception ignored) {
}
current = null;
}
}
/** Top-rounded face for the bottom tools sheet; same white surface as cards. */
static android.graphics.drawable.GradientDrawable sheetFace() {
android.graphics.drawable.GradientDrawable drawable =
new android.graphics.drawable.GradientDrawable();
drawable.setColor(Color.WHITE);
drawable.setCornerRadii(new float[] {48, 48, 48, 48, 0, 0, 0, 0});
drawable.setStroke(2, 0xFFE0E0E0);
return drawable;
}
static GradientDrawable surface() {
GradientDrawable drawable = new GradientDrawable();
drawable.setColor(Color.WHITE);
drawable.setCornerRadius(48);
drawable.setStroke(2, 0xFFE0E0E0);
return drawable;
}
static int dp(Activity activity, int value) {
return Math.round(value * activity.getResources().getDisplayMetrics().density);
}
static int id(Activity activity, String name) {
return activity
.getResources()
.getIdentifier(
name, name.startsWith("icp_") ? "drawable" : "id", activity.getPackageName());
}
/** Full-width hairline used between rows (launcher lists and panel rows). */
public static View hairline(Activity activity) {
View line = new View(activity);
line.setBackgroundColor(0xFFE0E0E0);
line.setLayoutParams(
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dp(activity, 1)));
return line;
}
}

View file

@ -0,0 +1,185 @@
package rip.crit.ms.moannotes.nativebridge;
import android.app.Activity;
import android.graphics.Color;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
/**
* Bottom docker listing every patch action in one place: Layers, Ink, snap toggle, Export KRA.
* Opened from the external tools FAB; the stock toolbar and menus are left alone. Row icons are
* official ByteDance IconPark ports (see res/drawable/icp_*.xml provenance headers).
*/
public final class ToolsSheet {
private static PopupWindow current;
private ToolsSheet() {}
public static void show(Activity activity) {
dismiss();
LinearLayout sheet = new LinearLayout(activity);
sheet.setOrientation(LinearLayout.VERTICAL);
sheet.setBackground(PanelDocker.sheetFace());
int hPad = PanelDocker.dp(activity, 8);
sheet.setPadding(hPad, PanelDocker.dp(activity, 8), hPad, PanelDocker.dp(activity, 16));
LinearLayout header = new LinearLayout(activity);
header.setGravity(Gravity.CENTER_VERTICAL);
header.setPadding(PanelDocker.dp(activity, 12), 0, PanelDocker.dp(activity, 4), 0);
TextView label = new TextView(activity);
label.setText("Tools");
label.setTextColor(Color.BLACK);
label.setTextSize(18);
header.addView(label, new LinearLayout.LayoutParams(0, -2, 1));
ImageButton close = new ImageButton(activity);
close.setContentDescription("Close tools");
close.setBackgroundColor(Color.TRANSPARENT);
int closeIcon = PanelDocker.id(activity, "icp_close");
if (closeIcon != 0) close.setImageResource(closeIcon);
int touch = PanelDocker.dp(activity, 44);
close.setLayoutParams(new LinearLayout.LayoutParams(touch, touch));
close.setOnClickListener(v -> dismiss());
header.addView(close);
sheet.addView(header);
sheet.addView(PanelDocker.hairline(activity));
sheet.addView(
row(
activity,
"icp_layers",
"Layers",
"Show, reorder and export layers",
v -> {
dismiss();
LayerPanel.show(activity);
}));
sheet.addView(PanelDocker.hairline(activity));
sheet.addView(
row(
activity,
"icp_ink",
"Ink",
"Monochrome pen color",
v -> {
dismiss();
InkPanel.show(activity);
}));
sheet.addView(PanelDocker.hairline(activity));
sheet.addView(snapRow(activity));
sheet.addView(PanelDocker.hairline(activity));
sheet.addView(
row(
activity,
"icp_kra_export",
"Export KRA",
"Krita paint layers",
v -> {
dismiss();
Object target = NativeFeatureHooks.findCanvas(activity);
if (target != null) KraExporter.exportCurrentPage(activity, target);
}));
PopupWindow window =
new PopupWindow(
sheet,
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
true);
window.setBackgroundDrawable(PanelDocker.sheetFace());
window.setOnDismissListener(() -> current = null);
current = window;
window.showAtLocation(activity.getWindow().getDecorView(), Gravity.BOTTOM, 0, 0);
}
static void dismiss() {
if (current != null) {
try {
current.dismiss();
} catch (Exception ignored) {
}
current = null;
}
}
private static LinearLayout snapRow(Activity activity) {
View handwrite = NativeFeatureHooks.findHandwriteView(activity);
Object listener =
handwrite == null ? null : NativeFeatureHooks.snapListener(activity, handwrite);
boolean enabled =
listener == null
? NativeFeatureHooks.snapEnabled(activity)
: NativeFeatureHooks.isSnapEnabled(listener);
LinearLayout row =
baseRow(
activity,
"icp_snap_line",
"Snap to line",
enabled ? "On: strokes stick to ruled baselines" : "Off");
ImageView state = new ImageView(activity);
int check = PanelDocker.id(activity, "icp_check");
if (enabled && check != 0) {
state.setImageResource(check);
state.setContentDescription("Snap on");
} else {
state.setContentDescription("Snap off");
}
int trailing = PanelDocker.dp(activity, 28);
state.setLayoutParams(new LinearLayout.LayoutParams(trailing, trailing));
state.setAlpha(enabled ? 1.0f : 0.35f);
row.addView(state);
row.setOnClickListener(
v -> {
View target = NativeFeatureHooks.findHandwriteView(activity);
if (target == null) return;
Object active = NativeFeatureHooks.snapListener(activity, target);
if (active == null) return;
boolean next = !NativeFeatureHooks.isSnapEnabled(active);
NativeFeatureHooks.setSnapEnabled(activity, active, next);
show(activity);
});
return row;
}
private static LinearLayout row(
Activity activity, String icon, String title, String subtitle, View.OnClickListener action) {
LinearLayout row = baseRow(activity, icon, title, subtitle);
row.setOnClickListener(action);
return row;
}
private static LinearLayout baseRow(
Activity activity, String icon, String title, String subtitle) {
LinearLayout row = new LinearLayout(activity);
row.setGravity(Gravity.CENTER_VERTICAL);
int vPad = PanelDocker.dp(activity, 10);
row.setPadding(PanelDocker.dp(activity, 12), vPad, PanelDocker.dp(activity, 12), vPad);
ImageView glyph = new ImageView(activity);
int iconId = PanelDocker.id(activity, icon);
if (iconId != 0) glyph.setImageResource(iconId);
int glyphSize = PanelDocker.dp(activity, 28);
LinearLayout.LayoutParams glyphParams = new LinearLayout.LayoutParams(glyphSize, glyphSize);
glyphParams.rightMargin = PanelDocker.dp(activity, 16);
row.addView(glyph, glyphParams);
LinearLayout texts = new LinearLayout(activity);
texts.setOrientation(LinearLayout.VERTICAL);
TextView name = new TextView(activity);
name.setText(title);
name.setTextColor(Color.BLACK);
name.setTextSize(16);
texts.addView(name);
TextView hint = new TextView(activity);
hint.setText(subtitle);
hint.setTextColor(0xFF616161);
hint.setTextSize(13);
texts.addView(hint);
row.addView(texts, new LinearLayout.LayoutParams(0, -2, 1));
return row;
}
}

View file

@ -21,7 +21,7 @@
"scripts": { "scripts": {
"postinstall": "patch-package", "postinstall": "patch-package",
"pp": "git submodule update --init --recursive && mkdir -p java/ && cp -r apk_unpack/java/* java/ && cp -r apk_unpack/cli . && patch-package apk_unpack", "pp": "git submodule update --init --recursive && mkdir -p java/ && cp -r apk_unpack/java/* java/ && cp -r apk_unpack/cli . && patch-package apk_unpack",
"format": "./jsfmt.sh && ./jfmt.sh && ./qmlfmt.sh && ./cppfmt.sh && ./rustfmt.sh", "format": "./jsfmt.sh && ./jfmt.sh && ./cppfmt.sh && ./rustfmt.sh",
"patch-apks": "bun run index.ts", "patch-apks": "bun run index.ts",
"extract-eink": "bun run extractors/eink.ts", "extract-eink": "bun run extractors/eink.ts",
"kr": "bun run scripts/krita-patch.ts", "kr": "bun run scripts/krita-patch.ts",
@ -37,6 +37,7 @@
"sign:moanlauncher": "bun run scripts/sign-moannotes.ts MoanLauncher", "sign:moanlauncher": "bun run scripts/sign-moannotes.ts MoanLauncher",
"patch:moanlauncher": "bun run patch-apks MoanLauncher && bun run sign:moanlauncher", "patch:moanlauncher": "bun run patch-apks MoanLauncher && bun run sign:moanlauncher",
"reinstall:moannotes": "bun run scripts/reinstall-moannotes.ts", "reinstall:moannotes": "bun run scripts/reinstall-moannotes.ts",
"reinstall:moanlauncher": "bun run scripts/reinstall-moannotes.ts --app MoanLauncher",
"adbme": "bun run scripts/adbme.ts", "adbme": "bun run scripts/adbme.ts",
"translator:test": "python3 -m unittest discover -s translator -p 'test_*.py'" "translator:test": "python3 -m unittest discover -s translator -p 'test_*.py'"
}, },

View file

@ -1,25 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="54dp" android:layout_height="wrap_content"
android:minHeight="58dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="vertical"
android:paddingStart="22dp" android:paddingStart="22dp"
android:paddingEnd="22dp"> android:paddingEnd="22dp"
<ImageView android:paddingTop="9dp"
android:id="@id/iv_app_icon" android:paddingBottom="9dp">
android:layout_width="32dp" <LinearLayout
android:layout_height="32dp" android:layout_width="match_parent"
android:contentDescription="App icon"
android:scaleType="centerInside" />
<TextView
android:id="@id/tv_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp" android:gravity="center_vertical"
android:layout_weight="1" android:orientation="horizontal">
android:ellipsize="end" <ImageView
android:singleLine="true" android:id="@id/iv_app_icon"
android:textColor="@color/textcolor1319" android:layout_width="34dp"
android:textSize="17sp" /> android:layout_height="34dp"
android:contentDescription="App icon"
android:scaleType="centerInside" />
<TextView
android:id="@id/tv_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/textcolor1319"
android:textSize="17sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="9dp"
android:background="#ffe0e0e0" />
</LinearLayout> </LinearLayout>

View file

@ -1,25 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="72dp" android:layout_height="wrap_content"
android:minHeight="76dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="vertical"
android:paddingStart="24dp" android:paddingStart="24dp"
android:paddingEnd="24dp"> android:paddingEnd="24dp"
<ImageView android:paddingTop="12dp"
android:id="@id/iv_app_icon" android:paddingBottom="12dp">
android:layout_width="42dp" <LinearLayout
android:layout_height="42dp" android:layout_width="match_parent"
android:contentDescription="App icon"
android:scaleType="centerInside" />
<TextView
android:id="@id/tv_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="18dp" android:gravity="center_vertical"
android:layout_weight="1" android:orientation="horizontal">
android:ellipsize="end" <ImageView
android:singleLine="true" android:id="@id/iv_app_icon"
android:textColor="@color/black" android:layout_width="44dp"
android:textSize="19sp" /> android:layout_height="44dp"
android:contentDescription="App icon"
android:scaleType="centerInside" />
<TextView
android:id="@id/tv_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/black"
android:textSize="18sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="#ffe0e0e0" />
</LinearLayout> </LinearLayout>

View file

@ -34,3 +34,66 @@
invoke-interface {v0}, Landroid/content/SharedPreferences$Editor;->apply()V invoke-interface {v0}, Landroid/content/SharedPreferences$Editor;->apply()V
return-void return-void
.end method .end method
.method public static resolveNotesIntent(Landroid/content/pm/PackageManager;)Landroid/content/Intent;
.locals 2
# Stock Notes first so the original package keeps working, then the
# current patch build, then older renamed builds. Null means "no Notes
# installed", never crash.
const-string v0, "com.moan.notes"
invoke-virtual {p0, v0}, Landroid/content/pm/PackageManager;->getLaunchIntentForPackage(Ljava/lang/String;)Landroid/content/Intent;
move-result-object v0
if-nez v0, :done
const-string v1, "rip.crit.ms.com.moan.notes"
invoke-virtual {p0, v1}, Landroid/content/pm/PackageManager;->getLaunchIntentForPackage(Ljava/lang/String;)Landroid/content/Intent;
move-result-object v0
if-nez v0, :done
const-string v1, "rip.crit.ms.notes.com.moan.notes"
invoke-virtual {p0, v1}, Landroid/content/pm/PackageManager;->getLaunchIntentForPackage(Ljava/lang/String;)Landroid/content/Intent;
move-result-object v0
:done
return-object v0
.end method
.method public static safeStart(Landroid/content/Context;Landroid/content/Intent;)V
.locals 2
# Every external launch in the launcher funnels through here: null
# intents and unresolvable targets become a toast instead of a crash.
if-eqz p1, :toast
:try_start
invoke-virtual {p0, p1}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V
:try_end
.catchall {:try_start .. :try_end} :caught
return-void
:caught
move-exception v0
:toast
const-string v0, "Cannot open this item"
const/4 v1, 0x0
invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
return-void
.end method

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,23 @@
<!-- Ported from ByteDance IconPark "check-one" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24 44C29.5228 44 34.5228 41.7614 38.1421 38.1421C41.7614 34.5228 44 29.5228 44 24C44 18.4772 41.7614 13.4772 38.1421 9.85786C34.5228 6.23858 29.5228 4 24 4C18.4772 4 13.4772 6.23858 9.85786 9.85786C6.23858 13.4772 4 18.4772 4 24C4 29.5228 6.23858 34.5228 9.85786 38.1421C13.4772 41.7614 18.4772 44 24 44Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
<path
android:pathData="M16 24L22 30L34 18"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -0,0 +1,24 @@
<!-- Ported from ByteDance IconPark "close" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M8 8L40 40"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M8 40L40 8"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -1,3 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF1A1A1A" android:pathData="M13,3v8.2l2.6,-2.6L17,10l-5,5 -5,-5 1.4,-1.4 2.6,2.6V3h2zM5,17h14v4H5v-4z"/>
</vector>

View file

@ -0,0 +1,48 @@
<!-- Ported from ByteDance IconPark "preview-close" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M6 16C6.63472 17.2193 7.59646 18.3504 8.82276 19.3554C12.261 22.1733 17.779 24 24 24C30.221 24 35.739 22.1733 39.1772 19.3554C40.4035 18.3504 41.3653 17.2193 42 16"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M28.9775 24L31.048 31.7274"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M37.3535 21.3536L43.0103 27.0104"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M5.00004 27.0103L10.6569 21.3534"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M16.9278 31.7276L18.9983 24.0001"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -0,0 +1,22 @@
<!-- Ported from ByteDance IconPark "preview-open" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24 36C35.0457 36 44 24 44 24C44 24 35.0457 12 24 12C12.9543 12 4 24 4 24C4 24 12.9543 36 24 36Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
<path
android:pathData="M24 29C26.7614 29 29 26.7614 29 24C29 21.2386 26.7614 19 24 19C21.2386 19 19 21.2386 19 24C19 26.7614 21.2386 29 24 29Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -0,0 +1,23 @@
<!-- Ported from ByteDance IconPark "write" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M5.32497 43.4996L13.81 43.4998L44.9227 12.3871L36.4374 3.90186L5.32471 35.0146L5.32497 43.4996Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
<path
android:pathData="M27.9521 12.3872L36.4374 20.8725"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -1 +1,32 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M5,3L15,3L20,8L20,21L5,21ZM15,4L15,9L19,9M12,11L12,17M9,14L15,14"/></vector> <!-- Ported from ByteDance IconPark "export" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M42 27C42 33 38 43 24 43C10 43 6 33 6 27"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M24.0078 5.10059V33.0001"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M12 17L24 5L36 17"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -1 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M3,5L21,5L21,7L3,7M3,11L21,11L21,13L3,13M3,17L21,17L21,19L3,19"/></vector> <!-- Ported from ByteDance IconPark "layers" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M4 11.9143L24 19L44 11.9143L24 5L4 11.9143Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
<path
android:pathData="M4 20L24 27L44 20"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M4 28L24 35L44 28"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M4 36L24 43L44 36"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -1,3 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF1A1A1A" android:pathData="M12,3C6.5,3 2,6.8 2,11.5S6.5,20 12,20h1.2c1.5,0 2.2,-1.8 1.1,-2.9c-0.5,-0.5 -0.2,-1.4 0.5,-1.4H16c3.3,0 6,-2.7 6,-6C22,5.9 17.5,3 12,3zM7,11.5a1.3,1.3 0,1 1,0,-2.6 1.3,1.3 0,0 1,0,2.6zM11,8.5a1.3,1.3 0,1 1,0,-2.6 1.3,1.3 0,0 1,0,2.6zM16,9.5a1.3,1.3 0,1 1,0,-2.6 1.3,1.3 0,0 1,0,2.6z"/>
</vector>

View file

@ -1,3 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF1A1A1A" android:pathData="M4.4,16.7L4,20l3.3,-0.4L18.6,8.3l-2.9,-2.9L4.4,16.7zM17.1,4L19,2.1c0.4,-0.4 1,-0.4 1.4,0l0.5,0.5c0.4,0.4 0.4,1 0,1.4L20,5.9L17.1,4z"/>
</vector>

View file

@ -1,7 +1,32 @@
<!-- Ported from ByteDance IconPark "magnet" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="48"
android:viewportHeight="24"> android:viewportHeight="48">
<path android:fillColor="#FF000000" android:pathData="M4,5h16v1H4zM4,11h16v1H4zM4,17h16v1H4zM16.2,3.1l4.7,4.7 -6.5,6.5 -3.2,0.9 0.9,-3.2zM13,13.2l1.8,-0.5 4.7,-4.7 -1.3,-1.3 -4.7,4.7z" /> <path
android:pathData="M6 10V26C6 35.9411 14.0589 44 24 44C33.9411 44 42 35.9411 42 26V10M14 10V26C14 31.5228 18.4772 36 24 36C29.5228 36 34 31.5228 34 26V10"
android:fillColor="#00000000"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M14 4H6V10H14V4Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
<path
android:pathData="M34 4H42V10H34V4Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
/>
</vector> </vector>

View file

@ -0,0 +1,22 @@
<!-- Ported from ByteDance IconPark "setting-two" (@icon-park/svg 1.4.2, Apache-2.0).
48-grid outline theme, stroke-width 4, converted path-for-path to VectorDrawable. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M18.2838 43.1713C14.9327 42.1736 11.9498 40.3213 9.58787 37.867C10.469 36.8227 11 35.4734 11 34.0001C11 30.6864 8.31371 28.0001 5 28.0001C4.79955 28.0001 4.60139 28.01 4.40599 28.0292C4.13979 26.7277 4 25.3803 4 24.0001C4 21.9095 4.32077 19.8938 4.91579 17.9995C4.94381 17.9999 4.97188 18.0001 5 18.0001C8.31371 18.0001 11 15.3138 11 12.0001C11 11.0488 10.7786 10.1493 10.3846 9.35011C12.6975 7.1995 15.5205 5.59002 18.6521 4.72314C19.6444 6.66819 21.6667 8.00013 24 8.00013C26.3333 8.00013 28.3556 6.66819 29.3479 4.72314C32.4795 5.59002 35.3025 7.1995 37.6154 9.35011C37.2214 10.1493 37 11.0488 37 12.0001C37 15.3138 39.6863 18.0001 43 18.0001C43.0281 18.0001 43.0562 17.9999 43.0842 17.9995C43.6792 19.8938 44 21.9095 44 24.0001C44 25.3803 43.8602 26.7277 43.594 28.0292C43.3986 28.01 43.2005 28.0001 43 28.0001C39.6863 28.0001 37 30.6864 37 34.0001C37 35.4734 37.531 36.8227 38.4121 37.867C36.0502 40.3213 33.0673 42.1736 29.7162 43.1713C28.9428 40.752 26.676 39.0001 24 39.0001C21.324 39.0001 19.0572 40.752 18.2838 43.1713Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
<path
android:pathData="M24 31C27.866 31 31 27.866 31 24C31 20.134 27.866 17 24 17C20.134 17 17 20.134 17 24C17 27.866 20.134 31 24 31Z"
android:fillColor="#FFFFFFFF"
android:strokeColor="#FF1A1A1A"
android:strokeWidth="4"
android:strokeLineJoin="round"
/>
</vector>

View file

@ -1,5 +1,3 @@
<resources> <resources>
<item name="moannotes_snap_line_button" type="id" /> <item name="moannotes_tools_fab" type="id" />
<item name="moannotes_layers_button" type="id" />
<item name="moannotes_kra_export_button" type="id" />
</resources> </resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="college_ruled">College ruled</string>
</resources>

View file

@ -2,31 +2,105 @@
.super Ljava/lang/Object; .super Ljava/lang/Object;
.method public static appendCollegeRuled(Ljava/util/ArrayList;)V .method public static appendCollegeRuled(Ljava/util/ArrayList;)V
.locals 4 .locals 6
if-eqz p0, :return
# Guard against double registration: the catalog builder can run more
# than once per process, and duplicates used to pile up rows.
invoke-virtual {p0}, Ljava/util/ArrayList;->size()I
move-result v0
const/4 v1, 0x0
:loop
if-ge v1, v0, :add
invoke-virtual {p0, v1}, Ljava/util/ArrayList;->get(I)Ljava/lang/Object;
move-result-object v2
check-cast v2, Lh/f/b/m5/l;
iget-object v2, v2, Lh/f/b/m5/l;->d:Ljava/lang/String;
const-string v3, "College ruled"
invoke-virtual {v3, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v4
if-eqz v4, :next
return-void
:next
add-int/lit8 v1, v1, 0x1
goto :loop
:add
# Resolve REAL resource ids. Two other adapters (background apply list
# and page-thumb picker) call getString(e) / setImageResource(c)
# unconditionally, so a 0 in either field crashes with
# Resources$NotFoundException the moment the row binds. Skip the entry
# if either id is missing instead of crashing.
sget-object v0, Lcom/moan/notes/NoteApp;->e:Landroid/content/Context;
if-eqz v0, :return
invoke-virtual {v0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v1
invoke-virtual {v0}, Landroid/content/Context;->getPackageName()Ljava/lang/String;
move-result-object v0
const-string v2, "template_college_ruled"
const-string v3, "drawable"
invoke-virtual {v1, v2, v3, v0}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result v2
if-eqz v2, :return
const-string v3, "college_ruled"
const-string v4, "string"
invoke-virtual {v1, v3, v4, v0}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result v1
if-eqz v1, :return
new-instance v0, Lh/f/b/m5/l; new-instance v0, Lh/f/b/m5/l;
invoke-direct {v0}, Lh/f/b/m5/l;-><init>()V invoke-direct {v0}, Lh/f/b/m5/l;-><init>()V
const-string v1, "background" const-string v3, "background"
invoke-static {v1}, Lh/a/a/a/a;->m(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-static {v3}, Lh/a/a/a/a;->m(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1 move-result-object v3
sget-object v2, Ljava/io/File;->separator:Ljava/lang/String; sget-object v4, Ljava/io/File;->separator:Ljava/lang/String;
const-string v3, "template_college_ruled.png" const-string v5, "template_college_ruled.png"
invoke-static {v1, v2, v3}, Lh/a/a/a/a;->j(Ljava/lang/StringBuilder;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; invoke-static {v3, v4, v5}, Lh/a/a/a/a;->j(Ljava/lang/StringBuilder;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v1 move-result-object v3
iput-object v1, v0, Lh/f/b/m5/l;->b:Ljava/lang/String; iput-object v3, v0, Lh/f/b/m5/l;->b:Ljava/lang/String;
const/4 v1, 0x0 iput v2, v0, Lh/f/b/m5/l;->c:I
iput v1, v0, Lh/f/b/m5/l;->c:I iput v1, v0, Lh/f/b/m5/l;->e:I
const-string v1, "College ruled" const-string v1, "College ruled"
@ -34,11 +108,12 @@
invoke-virtual {p0, v0}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z invoke-virtual {p0, v0}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
:return
return-void return-void
.end method .end method
.method public static previewResource(Landroid/content/Context;Ljava/lang/String;I)I .method public static previewResource(Landroid/content/Context;Ljava/lang/String;I)I
.locals 4 .locals 5
const-string v0, "College ruled" const-string v0, "College ruled"
@ -48,6 +123,7 @@
if-eqz v0, :cond_0 if-eqz v0, :cond_0
:try_start
invoke-virtual {p0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; invoke-virtual {p0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v0 move-result-object v0
@ -62,8 +138,20 @@
invoke-virtual {v0, v1, v2, v3}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I invoke-virtual {v0, v1, v2, v3}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result p2 move-result v0
:try_end
.catchall {:try_start .. :try_end} :fallback
# Never hand a 0 back: setImageResource(0) throws and used to crash the
# background grid the moment College ruled scrolled into view. Fall back
# to the stock preview (or id) the caller already had.
if-eqz v0, :fallback
move p2, v0
goto :cond_0
:fallback
:cond_0 :cond_0
return p2 return p2
.end method .end method

View file

@ -18,7 +18,7 @@
const-string v1, "feature_manifest_version" const-string v1, "feature_manifest_version"
const/4 v2, 0x1 const/4 v2, 0x2
invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences$Editor;->putInt(Ljava/lang/String;I)Landroid/content/SharedPreferences$Editor; invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences$Editor;->putInt(Ljava/lang/String;I)Landroid/content/SharedPreferences$Editor;
@ -29,182 +29,13 @@
return-void return-void
.end method .end method
.method private static dp(Landroid/content/Context;I)I
.locals 2
invoke-virtual {p0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object p0
invoke-virtual {p0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
move-result-object p0
iget p0, p0, Landroid/util/DisplayMetrics;->density:F
int-to-float p1, p1
mul-float/2addr p0, p1
invoke-static {p0}, Ljava/lang/Math;->round(F)I
move-result p0
return p0
.end method
.method public static installNote(Landroid/app/Activity;)V .method public static installNote(Landroid/app/Activity;)V
.locals 12 .locals 1
invoke-virtual {p0}, Landroid/app/Activity;->getResources()Landroid/content/res/Resources;
move-result-object v0
const-string v1, "moannotes_snap_line_button"
const-string v2, "id"
invoke-virtual {p0}, Landroid/app/Activity;->getPackageName()Ljava/lang/String;
move-result-object v3
invoke-virtual {v0, v1, v2, v3}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result v1
invoke-virtual {p0, v1}, Landroid/app/Activity;->findViewById(I)Landroid/view/View;
move-result-object v1
if-eqz v1, :cond_0
return-void
:cond_0
const-string v1, "note_handwrite_view"
const-string v2, "id"
invoke-virtual {p0}, Landroid/app/Activity;->getPackageName()Ljava/lang/String;
move-result-object v3
invoke-virtual {v0, v1, v2, v3}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result v1
invoke-virtual {p0, v1}, Landroid/app/Activity;->findViewById(I)Landroid/view/View;
move-result-object v1
if-eqz v1, :cond_2
const-string v2, "moannotes_patch"
const/4 v3, 0x0
invoke-virtual {p0, v2, v3}, Landroid/app/Activity;->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;
move-result-object v2
const-string v4, "snap_to_line_enabled"
invoke-interface {v2, v4, v3}, Landroid/content/SharedPreferences;->getBoolean(Ljava/lang/String;Z)Z
move-result v4
new-instance v5, Lrip/crit/ms/moannotes/SnapToLineTouchListener;
invoke-direct {v5, p0, v4}, Lrip/crit/ms/moannotes/SnapToLineTouchListener;-><init>(Landroid/content/Context;Z)V
invoke-virtual {v1, v5}, Landroid/view/View;->setOnTouchListener(Landroid/view/View$OnTouchListener;)V
const-string v1, "edit_button_bar"
const-string v6, "id"
invoke-virtual {p0}, Landroid/app/Activity;->getPackageName()Ljava/lang/String;
move-result-object v7
invoke-virtual {v0, v1, v6, v7}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result v1
invoke-virtual {p0, v1}, Landroid/app/Activity;->findViewById(I)Landroid/view/View;
move-result-object v1
instance-of v6, v1, Landroid/widget/LinearLayout;
if-eqz v6, :cond_2
check-cast v1, Landroid/widget/LinearLayout;
new-instance v6, Landroid/widget/ImageButton;
invoke-direct {v6, p0}, Landroid/widget/ImageButton;-><init>(Landroid/content/Context;)V
invoke-virtual {v6, v3}, Landroid/widget/ImageButton;->setBackgroundColor(I)V
const/16 v7, 0x30
invoke-static {p0, v7}, Lrip/crit/ms/moannotes/FeatureRegistry;->dp(Landroid/content/Context;I)I
move-result v7
new-instance v8, Landroid/widget/LinearLayout$LayoutParams;
const/4 v9, -0x1
invoke-direct {v8, v7, v9}, Landroid/widget/LinearLayout$LayoutParams;-><init>(II)V
invoke-virtual {v6, v8}, Landroid/widget/ImageButton;->setLayoutParams(Landroid/view/ViewGroup$LayoutParams;)V
const-string v7, "icp_snap_line"
const-string v8, "drawable"
invoke-virtual {p0}, Landroid/app/Activity;->getPackageName()Ljava/lang/String;
move-result-object v9
invoke-virtual {v0, v7, v8, v9}, Landroid/content/res/Resources;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
move-result v0
invoke-virtual {v6, v0}, Landroid/widget/ImageButton;->setImageResource(I)V
const-string v0, "Handwriting snap to line"
invoke-virtual {v6, v0}, Landroid/widget/ImageButton;->setContentDescription(Ljava/lang/CharSequence;)V
invoke-virtual {v6, v1}, Landroid/widget/ImageButton;->setId(I)V
if-eqz v4, :cond_1
const/high16 v0, 0x3f800000 # 1.0f
goto :goto_0
:cond_1
const v0, 0x3ee66666 # 0.45f
:goto_0
invoke-virtual {v6, v0}, Landroid/widget/ImageButton;->setAlpha(F)V
new-instance v0, Lrip/crit/ms/moannotes/SnapToLineToggle;
invoke-direct {v0, v5, v2, v6}, Lrip/crit/ms/moannotes/SnapToLineToggle;-><init>(Lrip/crit/ms/moannotes/SnapToLineTouchListener;Landroid/content/SharedPreferences;Landroid/widget/ImageButton;)V
invoke-virtual {v6, v0}, Landroid/widget/ImageButton;->setOnClickListener(Landroid/view/View$OnClickListener;)V
const/4 v0, 0x7
invoke-virtual {v1, v6, v0}, Landroid/widget/LinearLayout;->addView(Landroid/view/View;I)V
# Single integration point: the Java bridge appends the layers / ink /
# snap buttons (guarded against duplicates) and owns the snap listener.
# Nothing here touches stock toolbar icons.
invoke-static {p0}, Lrip/crit/ms/moannotes/nativebridge/NativeFeatureHooks;->install(Landroid/app/Activity;)V invoke-static {p0}, Lrip/crit/ms/moannotes/nativebridge/NativeFeatureHooks;->install(Landroid/app/Activity;)V
:cond_2
return-void return-void
.end method .end method

View file

@ -26,7 +26,9 @@
iget p1, p1, Landroid/util/DisplayMetrics;->density:F iget p1, p1, Landroid/util/DisplayMetrics;->density:F
const/high16 v0, 0x42600000 # 56.0f # 64dp matches the regenerated college-ruled sheet (120px lines at the
# native 1404px page width, ~1.875x e-ink density bucket).
const/high16 v0, 0x42800000 # 64.0f
mul-float/2addr p1, v0 mul-float/2addr p1, v0
@ -56,97 +58,161 @@
.end method .end method
.method public onTouch(Landroid/view/View;Landroid/view/MotionEvent;)Z .method public onTouch(Landroid/view/View;Landroid/view/MotionEvent;)Z
.locals 6 .locals 8
iget-boolean p1, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->enabled:Z iget-boolean v0, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->enabled:Z
if-eqz p1, :cond_5 if-nez v0, :cond_enabled
const/4 p1, 0x0 goto :pass
invoke-virtual {p2, p1}, Landroid/view/MotionEvent;->getToolType(I)I :cond_enabled
# Scan every pointer for a stylus (tool type 2). The old code only looked
# at index 0, so a stylus arriving as a second pointer never snapped.
invoke-virtual {p2}, Landroid/view/MotionEvent;->getPointerCount()I
move-result v0 move-result v0
const/4 v1, 0x2 const/4 v1, 0x0
if-ne v0, v1, :cond_5 const/4 v2, -0x1
invoke-virtual {p2}, Landroid/view/MotionEvent;->getActionMasked()I :scan
if-ge v1, v0, :scanned
move-result v0 invoke-virtual {p2, v1}, Landroid/view/MotionEvent;->getToolType(I)I
if-ne v0, p1, :cond_0
invoke-virtual {p2}, Landroid/view/MotionEvent;->getY()F
move-result v1
iget v2, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->spacing:F
div-float v3, v1, v2
invoke-static {v3}, Ljava/lang/Math;->round(F)I
move-result v3 move-result v3
int-to-float v3, v3 const/4 v4, 0x2
mul-float/2addr v3, v2 if-ne v3, v4, :scan_next
sub-float/2addr v3, v1 move v2, v1
iput v3, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->offset:F goto :scanned
:cond_0 :scan_next
const/4 v1, 0x1 add-int/lit8 v1, v1, 0x1
if-eq v0, p1, :cond_1 goto :scan
if-eq v0, v1, :cond_1 :scanned
const/4 v0, -0x1
const/4 p1, 0x2 if-eq v2, v0, :pass
if-eq v0, p1, :cond_1 invoke-virtual {p2}, Landroid/view/MotionEvent;->getActionMasked()I
const/4 p1, 0x3 move-result v3
if-ne v0, p1, :cond_5 invoke-virtual {p2, v2}, Landroid/view/MotionEvent;->getX(I)F
:cond_1 move-result v4
invoke-virtual {p2}, Landroid/view/MotionEvent;->getX()F
move-result p1 invoke-virtual {p2, v2}, Landroid/view/MotionEvent;->getY(I)F
invoke-virtual {p2}, Landroid/view/MotionEvent;->getY()F move-result v5
move-result v2 # (Re)compute the frozen stroke offset whenever a pointer goes down, so a
# stylus joining late still snaps. The frozen offset preserves the
# internal letter shape for the whole stroke.
if-nez v3, :not_down
iget v3, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->offset:F goto :recompute
add-float/2addr v2, v3 :not_down
const/4 v0, 0x5
invoke-virtual {p2, p1, v2}, Landroid/view/MotionEvent;->setLocation(FF)V if-ne v3, v0, :apply_check
if-ne v0, v1, :cond_3 :recompute
iget v0, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->spacing:F
:cond_2 div-float v1, v5, v0
const/4 p1, 0x0
iput p1, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->offset:F invoke-static {v1}, Ljava/lang/Math;->round(F)I
goto :goto_0 move-result v1
:cond_3 int-to-float v1, v1
const/4 p1, 0x3
if-ne v0, p1, :cond_5 mul-float/2addr v1, v0
goto :cond_2 sub-float/2addr v1, v5
:cond_5 iput v1, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->offset:F
:goto_0
const/4 p1, 0x0
return p1 :apply_check
const/4 v0, 0x2
if-ne v3, v0, :check_down
goto :apply
:check_down
if-nez v3, :check_up
goto :apply
:check_up
const/4 v0, 0x1
if-ne v3, v0, :check_cancel
goto :apply
:check_cancel
const/4 v0, 0x3
if-ne v3, v0, :check_pdown
goto :apply
:check_pdown
const/4 v0, 0x5
if-ne v3, v0, :check_pup
goto :apply
:check_pup
const/4 v0, 0x6
if-ne v3, v0, :pass
:apply
iget v0, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->offset:F
add-float/2addr v5, v0
invoke-virtual {p2, v4, v5}, Landroid/view/MotionEvent;->setLocation(FF)V
const/4 v0, 0x1
if-ne v3, v0, :check_cancel2
goto :clear
:check_cancel2
const/4 v0, 0x3
if-ne v3, v0, :check_pup2
goto :clear
:check_pup2
const/4 v0, 0x6
if-ne v3, v0, :pass
:clear
const/4 v0, 0x0
iput v0, p0, Lrip/crit/ms/moannotes/SnapToLineTouchListener;->offset:F
:pass
const/4 v0, 0x0
return v0
.end method .end method

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
git ls-files -z --cached --others --exclude-standard | grep -zE '\.(qml|js)$' | xargs -0 qmlformat -i
echo "qml/done"

View file

@ -5,6 +5,35 @@ import path from "node:path";
const root = path.resolve(import.meta.dir, ".."); const root = path.resolve(import.meta.dir, "..");
const args = process.argv.slice(2); const args = process.argv.slice(2);
const app = value("--app") ?? "MoanNote";
const profiles: Record<
string,
{
patchScript: string;
legacyPackage: string;
patchedPackage: string;
removeLegacy: boolean;
legacySystemImage: boolean;
}
> = {
MoanNote: {
patchScript: "patch:moannotes",
legacyPackage: "com.moan.notes",
patchedPackage: "rip.crit.ms.com.moan.notes",
removeLegacy: false,
legacySystemImage: true,
},
MoanLauncher: {
patchScript: "patch:moanlauncher",
legacyPackage: "com.note.launcher",
patchedPackage: "rip.crit.ms.com.note.launcher",
removeLegacy: false,
legacySystemImage: true,
},
};
const profile = profiles[app];
if (!profile) throw new Error(`Unsupported app ${app}`);
function value(flag: string): string | undefined { function value(flag: string): string | undefined {
const index = args.indexOf(flag); const index = args.indexOf(flag);
return index >= 0 ? args[index + 1] : undefined; return index >= 0 ? args[index + 1] : undefined;
@ -20,21 +49,82 @@ function run(command: string, commandArgs: string[]): void {
throw new Error(`${command} ${commandArgs.join(" ")} failed`); throw new Error(`${command} ${commandArgs.join(" ")} failed`);
} }
function uninstallIfPresent(serial: string, packageName: string): void {
const result = spawnSync("adb", ["-s", serial, "uninstall", packageName], {
cwd: root,
encoding: "utf8",
});
if (result.error) throw result.error;
if (result.status === 0) {
if (result.stdout) process.stdout.write(result.stdout);
return;
}
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
if (/not installed/i.test(output)) {
console.log(`[info] ${packageName} was not installed`);
return;
}
// OEM apps in /system cannot be uninstalled from the system partition. For a
// clean, per-user replacement, Package Manager can remove that system app
// for user 0. This also clears its user data.
if (/DELETE_FAILED_INTERNAL_ERROR|unknown system package/i.test(output)) {
const perUserResult = spawnSync(
"adb",
["-s", serial, "shell", "pm", "uninstall", "--user", "0", packageName],
{ cwd: root, encoding: "utf8" },
);
if (perUserResult.error) throw perUserResult.error;
if (perUserResult.status === 0) {
if (perUserResult.stdout) process.stdout.write(perUserResult.stdout);
console.log(`[info] Removed system app ${packageName} for user 0`);
return;
}
const perUserOutput = `${perUserResult.stdout ?? ""}${perUserResult.stderr ?? ""}`;
if (/not installed/i.test(perUserOutput)) {
console.log(`[info] ${packageName} was not installed for user 0`);
return;
}
if (perUserOutput) process.stderr.write(perUserOutput);
}
if (output) process.stderr.write(output);
throw new Error(`Unable to remove ${packageName}`);
}
function requireConnectedDevice(serial: string): void {
const result = spawnSync("adb", ["-s", serial, "get-state"], {
cwd: root,
encoding: "utf8",
});
if (result.error) throw result.error;
if (result.status !== 0 || result.stdout.trim() !== "device")
throw new Error(`ADB device ${serial} is not connected and authorized`);
}
const serial = value("--serial"); const serial = value("--serial");
if (!serial) if (!serial)
throw new Error("Specify the Xiaomi target with --serial <adb-serial>"); throw new Error("Specify the Xiaomi target with --serial <adb-serial>");
if (!args.includes("--confirm-reinstall")) if (!args.includes("--confirm-reinstall"))
throw new Error("Refusing to replace an app without --confirm-reinstall"); throw new Error("Refusing to replace an app without --confirm-reinstall");
run("bun", ["run", "patch:moannotes"]); requireConnectedDevice(serial);
run("bun", ["run", profile.patchScript]);
const apk = const apk =
value("--apk") ?? path.join(root, "outputs", "MoanNote_patched-signed.apk"); value("--apk") ?? path.join(root, "outputs", `${app}_patched-signed.apk`);
if (!existsSync(apk)) { if (!existsSync(apk)) {
throw new Error( throw new Error(
`Signed APK is missing at ${path.join("outputs", "MoanNote_patched-signed.apk")}`, `Signed APK is missing at ${path.join("outputs", `${app}_patched-signed.apk`)}`,
); );
} }
run("adb", ["-s", serial, "install", "--no-incremental", "-r", apk]); // Android can reject an uninstall of an already-patched package with
// DELETE_FAILED_INTERNAL_ERROR, even though `adb install -r` updates the same
// package normally. Keep the patched app in place and only remove the original
// package when its unchanged provider authority would conflict with the patch.
if (profile.removeLegacy) {
uninstallIfPresent(serial, profile.legacyPackage);
} else {
console.log(`[info] Keeping protected OEM package ${profile.legacyPackage}`);
}
run("adb", ["-s", serial, "install", "-r", "--no-incremental", apk]);
console.log( console.log(
`[ok] Reinstalled ${path.basename(apk)} without uninstalling MoanNotes data.`, `[ok] Installed ${path.basename(apk)}${profile.legacySystemImage ? ` alongside protected OEM system image ${profile.legacyPackage}` : profile.removeLegacy ? ` after replacing ${profile.legacyPackage}` : " as a side-by-side update"}.`,
); );

View file

@ -5,7 +5,8 @@ import { spawnSync } from "node:child_process";
const root = path.resolve(import.meta.dir, ".."); const root = path.resolve(import.meta.dir, "..");
const app = process.argv[2] ?? "MoanNote"; const app = process.argv[2] ?? "MoanNote";
if (!/^[A-Za-z0-9_-]+$/.test(app)) throw new Error("App name must be a simple file name"); if (!/^[A-Za-z0-9_-]+$/.test(app))
throw new Error("App name must be a simple file name");
const unsignedApk = path.join(root, "outputs", `${app}_patched.apk`); const unsignedApk = path.join(root, "outputs", `${app}_patched.apk`);
const signedApk = path.join(root, "outputs", `${app}_patched-signed.apk`); const signedApk = path.join(root, "outputs", `${app}_patched-signed.apk`);
const signingDir = path.join(root, "build", "signing"); const signingDir = path.join(root, "build", "signing");
@ -17,7 +18,9 @@ function run(command: string, args: string[]): void {
const result = spawnSync(command, args, { cwd: root, stdio: "inherit" }); const result = spawnSync(command, args, { cwd: root, stdio: "inherit" });
if (result.error) throw result.error; if (result.error) throw result.error;
if (result.status !== 0) if (result.status !== 0)
throw new Error(`${path.basename(command)} failed with status ${result.status}`); throw new Error(
`${path.basename(command)} failed with status ${result.status}`,
);
} }
function readCredentials(): Record<string, string> { function readCredentials(): Record<string, string> {
@ -32,7 +35,10 @@ function readCredentials(): Record<string, string> {
} }
function androidTool(name: "apksigner" | "zipalign"): string { function androidTool(name: "apksigner" | "zipalign"): string {
const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? "/var/home/hellings/android-sdk"; const sdk =
process.env.ANDROID_HOME ??
process.env.ANDROID_SDK_ROOT ??
"/var/home/hellings/android-sdk";
for (const version of ["36.0.0", "35.0.0", "34.0.0"]) { for (const version of ["36.0.0", "35.0.0", "34.0.0"]) {
const tool = path.join(sdk, "build-tools", version, name); const tool = path.join(sdk, "build-tools", version, name);
if (fs.existsSync(tool)) return tool; if (fs.existsSync(tool)) return tool;
@ -44,27 +50,72 @@ function ensureDevKey(): Record<string, string> {
fs.mkdirSync(signingDir, { recursive: true, mode: 0o700 }); fs.mkdirSync(signingDir, { recursive: true, mode: 0o700 });
if (!fs.existsSync(keystore)) { if (!fs.existsSync(keystore)) {
const password = crypto.randomBytes(32).toString("base64url"); const password = crypto.randomBytes(32).toString("base64url");
const values = { storePassword: password, keyPassword: password, alias: keyName }; const values = {
storePassword: password,
keyPassword: password,
alias: keyName,
};
run("keytool", [ run("keytool", [
"-genkeypair", "-keystore", keystore, "-storetype", "PKCS12", "-storepass", password, "-genkeypair",
"-alias", values.alias, "-keypass", password, "-keyalg", "RSA", "-keysize", "2048", "-keystore",
"-validity", "3650", "-dname", `CN=${app} Development,O=rip.crit.ms,C=US`, keystore,
"-storetype",
"PKCS12",
"-storepass",
password,
"-alias",
values.alias,
"-keypass",
password,
"-keyalg",
"RSA",
"-keysize",
"2048",
"-validity",
"3650",
"-dname",
`CN=${app} Development,O=rip.crit.ms,C=US`,
]); ]);
fs.writeFileSync(credentials, Object.entries(values).map(([key, value]) => `${key}=${value}`).join("\n") + "\n", { mode: 0o600 }); fs.writeFileSync(
credentials,
Object.entries(values)
.map(([key, value]) => `${key}=${value}`)
.join("\n") + "\n",
{ mode: 0o600 },
);
console.log(`[ok] Created local ${app} development key at ${keystore}`); console.log(`[ok] Created local ${app} development key at ${keystore}`);
} }
if (!fs.existsSync(credentials)) throw new Error(`Development key credentials are missing at ${credentials}`); if (!fs.existsSync(credentials))
throw new Error(
`Development key credentials are missing at ${credentials}`,
);
return readCredentials(); return readCredentials();
} }
if (!fs.existsSync(unsignedApk)) throw new Error(`Patched APK is missing at ${unsignedApk}`); if (!fs.existsSync(unsignedApk))
throw new Error(`Patched APK is missing at ${unsignedApk}`);
const values = ensureDevKey(); const values = ensureDevKey();
const aligned = path.join(root, "build", `${app}_patched-aligned.apk`); const aligned = path.join(root, "build", `${app}_patched-aligned.apk`);
fs.mkdirSync(path.dirname(aligned), { recursive: true }); fs.mkdirSync(path.dirname(aligned), { recursive: true });
run(androidTool("zipalign"), ["-f", "-p", "4", unsignedApk, aligned]); run(androidTool("zipalign"), ["-f", "-p", "4", unsignedApk, aligned]);
run(androidTool("apksigner"), [ run(androidTool("apksigner"), [
"sign", "--ks", keystore, "--ks-key-alias", values.alias!, "--ks-pass", `pass:${values.storePassword!}`, "sign",
"--key-pass", `pass:${values.keyPassword!}`, "--out", signedApk, aligned, "--ks",
keystore,
"--ks-key-alias",
values.alias!,
"--ks-pass",
`pass:${values.storePassword!}`,
"--key-pass",
`pass:${values.keyPassword!}`,
"--out",
signedApk,
aligned,
]);
run(androidTool("apksigner"), [
"verify",
"--verbose",
"--print-certs",
signedApk,
]); ]);
run(androidTool("apksigner"), ["verify", "--verbose", "--print-certs", signedApk]);
console.log(`[ok] Signed ${signedApk}`); console.log(`[ok] Signed ${signedApk}`);

View file

@ -1,65 +1,64 @@
package ntx.note.asynctask; package ntx.note.asynctask;
import java.lang.ref.WeakReference;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.widget.ImageView; import android.widget.ImageView;
import java.lang.ref.WeakReference;
public class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> { public class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference; private final WeakReference<ImageView> imageViewReference;
private int reqWidth = 0; private int reqWidth = 0;
private int reqHeight = 0; private int reqHeight = 0;
public BitmapWorkerTask(ImageView imageView, int reqWidth, int reqHeight) { public BitmapWorkerTask(ImageView imageView, int reqWidth, int reqHeight) {
// Use a WeakReference to ensure the ImageView can be garbage collected // Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView); imageViewReference = new WeakReference<ImageView>(imageView);
this.reqWidth = reqWidth; this.reqWidth = reqWidth;
this.reqHeight = reqHeight; this.reqHeight = reqHeight;
} }
@Override @Override
protected Bitmap doInBackground(String... params) { protected Bitmap doInBackground(String... params) {
if (!params[0].equals("")) { if (!params[0].equals("")) {
return resizeBitmapFromFile(params[0], reqWidth, reqHeight); return resizeBitmapFromFile(params[0], reqWidth, reqHeight);
} }
return null; return null;
} }
@Override @Override
protected void onPostExecute(Bitmap bitmap) { protected void onPostExecute(Bitmap bitmap) {
final ImageView imageView = imageViewReference.get(); final ImageView imageView = imageViewReference.get();
if (imageView != null && bitmap != null) { if (imageView != null && bitmap != null) {
imageView.setImageBitmap(bitmap); imageView.setImageBitmap(bitmap);
} }
} }
public Bitmap resizeBitmapFromFile(String path, int reqWidth, int reqHeight) { public Bitmap resizeBitmapFromFile(String path, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options(); final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options); BitmapFactory.decodeFile(path, options);
// Calculate inSampleSize Raw height and width of image // Calculate inSampleSize Raw height and width of image
final int height = options.outHeight; final int height = options.outHeight;
final int width = options.outWidth; final int width = options.outWidth;
options.inPreferredConfig = Bitmap.Config.RGB_565; options.inPreferredConfig = Bitmap.Config.RGB_565;
int inSampleSize = 1; int inSampleSize = 1;
if (height > reqHeight) { if (height > reqHeight) {
inSampleSize = Math.round((float) height / (float) reqHeight); inSampleSize = Math.round((float) height / (float) reqHeight);
} }
int expectedWidth = width / inSampleSize; int expectedWidth = width / inSampleSize;
if (expectedWidth > reqWidth) { if (expectedWidth > reqWidth) {
inSampleSize = Math.round((float) width / (float) reqWidth); inSampleSize = Math.round((float) width / (float) reqWidth);
} }
options.inSampleSize = inSampleSize; options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options); return BitmapFactory.decodeFile(path, options);
} }
} }

View file

@ -18,7 +18,6 @@ import android.widget.ImageView;
import android.widget.SimpleAdapter; import android.widget.SimpleAdapter;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -26,290 +25,298 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import ntx.note.asynctask.BitmapWorkerTask; import ntx.note.asynctask.BitmapWorkerTask;
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
public class ImagePickerActivity extends Activity implements OnItemClickListener { public class ImagePickerActivity extends Activity implements OnItemClickListener {
private final static String TAG = "ImagePickerActivity"; private static final String TAG = "ImagePickerActivity";
public String[] searchImageType = new String[]{".png", ".jpg", ".jpeg", ".bmp"}; //auto list files public String[] searchImageType =
new String[] {".png", ".jpg", ".jpeg", ".bmp"}; // auto list files
private final static int NumPerPage_Portrait = 9; //this should be fit real status private static final int NumPerPage_Portrait = 9; // this should be fit real status
private final static int NumPerPage_Landscape = 8; //this should be fit real status private static final int NumPerPage_Landscape = 8; // this should be fit real status
private final static int GridView_Landscape_height_6Inch = 830; private static final int GridView_Landscape_height_6Inch = 830;
private final static int GridView_Landscape_height_13Inch = 930; private static final int GridView_Landscape_height_13Inch = 930;
private int numPerPage = 0; //this should be fit real status private int numPerPage = 0; // this should be fit real status
protected File sdcardDir, externalCardDir = null; protected File sdcardDir, externalCardDir = null;
private GridView fileGrid; private GridView fileGrid;
private SimpleAdapter simpleAdapter; private SimpleAdapter simpleAdapter;
private List<String> resultPath = new ArrayList<String>(); private List<String> resultPath = new ArrayList<String>();
private List<Map<String, Object>> items = new ArrayList<Map<String, Object>>(); private List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
private TextView txtv_pageinfo; private TextView txtv_pageinfo;
private ImageView imgv_pre_page, imgv_next_page; private ImageView imgv_pre_page, imgv_next_page;
private int totalPage, currentPage = 0; private int totalPage, currentPage = 0;
public BitmapWorkerTask[] asyncTask = new BitmapWorkerTask[NumPerPage_Portrait]; public BitmapWorkerTask[] asyncTask = new BitmapWorkerTask[NumPerPage_Portrait];
public final String PATH_SDCARD = Environment.getExternalStorageDirectory().getPath(); public final String PATH_SDCARD = Environment.getExternalStorageDirectory().getPath();
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.image_picker); setContentView(R.layout.image_picker);
int display_mode = getResources().getConfiguration().orientation;
if (display_mode == Configuration.ORIENTATION_PORTRAIT) {
numPerPage = NumPerPage_Portrait;
} else {
numPerPage = NumPerPage_Landscape;
}
sdcardDir = new File(PATH_SDCARD);
searchFiles(sdcardDir);
Collections.sort(resultPath, new SortIgnoreCase());
totalPage = getTotalPage();
if (currentPage == 0)
currentPage = 1;
//add into items for showing
updateItems();
fileGrid = (GridView) findViewById(R.id.filepicker_file_grid);
if (display_mode == Configuration.ORIENTATION_PORTRAIT) {
} else {
ViewGroup.LayoutParams layoutParams = fileGrid.getLayoutParams();
if (NtxView.isEinkHandWritingHardwareType() && NtxView.isEink6InchHardwareType()) {
layoutParams.height = GridView_Landscape_height_6Inch;
} else {
layoutParams.height = GridView_Landscape_height_13Inch;
}
fileGrid.setLayoutParams(layoutParams);
}
simpleAdapter = new SimpleAdapter(this,
items, R.layout.image_picker_item, new String[]{"image", "text"},
new int[]{R.id.picker_image, R.id.picker_text}) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int mWidth = 205 * 2;
int mHeight = 150 * 2;
if (convertView == null) {
convertView = super.getView(position, convertView, parent);
}
final TextView nameView = (TextView) convertView.findViewById(R.id.picker_text);
final int indexNameS1 = items.get(position).get("image").toString().lastIndexOf('/') + 1;
final String filenameS1 = ((indexNameS1 > 0) ? items.get(position).get("image").toString().substring(indexNameS1).toLowerCase().intern() : "");
nameView.setText(filenameS1);
final ImageView coverView = (ImageView) convertView.findViewById(R.id.picker_image);
if (position > 0) {
coverView.setImageBitmap(null);
}
if (asyncTask[position] != null && asyncTask[position].getStatus().equals(Status.RUNNING)) {
asyncTask[position].cancel(true);
}
asyncTask[position] = new BitmapWorkerTask(coverView, mWidth, mHeight);
asyncTask[position].execute(items.get(position).get("image").toString());
return convertView;
}
};
fileGrid.setAdapter(simpleAdapter);
fileGrid.setOnItemClickListener(this);
if (resultPath.isEmpty()) {
Toast.makeText(ImagePickerActivity.this, R.string.image_pick_no_image, Toast.LENGTH_LONG).show();
}
setTitle(R.string.image_pick_title);
txtv_pageinfo = (TextView) findViewById(R.id.page_info);
imgv_pre_page = (ImageView) findViewById(R.id.imgb_pre_page);
imgv_next_page = (ImageView) findViewById(R.id.imgb_next_page);
imgv_pre_page.setOnClickListener(instantKeyListener);
imgv_next_page.setOnClickListener(instantKeyListener);
int display_mode = getResources().getConfiguration().orientation;
if (display_mode == Configuration.ORIENTATION_PORTRAIT) {
numPerPage = NumPerPage_Portrait;
} else {
numPerPage = NumPerPage_Landscape;
} }
private OnClickListener instantKeyListener = new OnClickListener() { sdcardDir = new File(PATH_SDCARD);
searchFiles(sdcardDir);
Collections.sort(resultPath, new SortIgnoreCase());
totalPage = getTotalPage();
if (currentPage == 0) currentPage = 1;
// add into items for showing
updateItems();
fileGrid = (GridView) findViewById(R.id.filepicker_file_grid);
if (display_mode == Configuration.ORIENTATION_PORTRAIT) {
} else {
ViewGroup.LayoutParams layoutParams = fileGrid.getLayoutParams();
if (NtxView.isEinkHandWritingHardwareType() && NtxView.isEink6InchHardwareType()) {
layoutParams.height = GridView_Landscape_height_6Inch;
} else {
layoutParams.height = GridView_Landscape_height_13Inch;
}
fileGrid.setLayoutParams(layoutParams);
}
simpleAdapter =
new SimpleAdapter(
this,
items,
R.layout.image_picker_item,
new String[] {"image", "text"},
new int[] {R.id.picker_image, R.id.picker_text}) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int mWidth = 205 * 2;
int mHeight = 150 * 2;
if (convertView == null) {
convertView = super.getView(position, convertView, parent);
}
final TextView nameView = (TextView) convertView.findViewById(R.id.picker_text);
final int indexNameS1 =
items.get(position).get("image").toString().lastIndexOf('/') + 1;
final String filenameS1 =
((indexNameS1 > 0)
? items
.get(position)
.get("image")
.toString()
.substring(indexNameS1)
.toLowerCase()
.intern()
: "");
nameView.setText(filenameS1);
final ImageView coverView = (ImageView) convertView.findViewById(R.id.picker_image);
if (position > 0) {
coverView.setImageBitmap(null);
}
if (asyncTask[position] != null
&& asyncTask[position].getStatus().equals(Status.RUNNING)) {
asyncTask[position].cancel(true);
}
asyncTask[position] = new BitmapWorkerTask(coverView, mWidth, mHeight);
asyncTask[position].execute(items.get(position).get("image").toString());
return convertView;
}
};
fileGrid.setAdapter(simpleAdapter);
fileGrid.setOnItemClickListener(this);
if (resultPath.isEmpty()) {
Toast.makeText(ImagePickerActivity.this, R.string.image_pick_no_image, Toast.LENGTH_LONG)
.show();
}
setTitle(R.string.image_pick_title);
txtv_pageinfo = (TextView) findViewById(R.id.page_info);
imgv_pre_page = (ImageView) findViewById(R.id.imgb_pre_page);
imgv_next_page = (ImageView) findViewById(R.id.imgb_next_page);
imgv_pre_page.setOnClickListener(instantKeyListener);
imgv_next_page.setOnClickListener(instantKeyListener);
}
private OnClickListener instantKeyListener =
new OnClickListener() {
// TODO // TODO
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
switch (v.getId()) { switch (v.getId()) {
case R.id.imgb_pre_page: case R.id.imgb_pre_page:
currentPage--; currentPage--;
if (currentPage <= 0) if (currentPage <= 0) currentPage = totalPage;
currentPage = totalPage;
txtv_pageinfo.setText(currentPage + " of " + totalPage); txtv_pageinfo.setText(currentPage + " of " + totalPage);
updateItems(); updateItems();
simpleAdapter.notifyDataSetChanged(); simpleAdapter.notifyDataSetChanged();
break; break;
case R.id.imgb_next_page: case R.id.imgb_next_page:
currentPage++; currentPage++;
if (currentPage > totalPage) if (currentPage > totalPage) currentPage = 1;
currentPage = 1;
txtv_pageinfo.setText(currentPage + " of " + totalPage); txtv_pageinfo.setText(currentPage + " of " + totalPage);
updateItems(); updateItems();
simpleAdapter.notifyDataSetChanged(); simpleAdapter.notifyDataSetChanged();
break; break;
default: default:
finish(); finish();
break; break;
}
}
};
@Override
public void onItemClick(AdapterView<?> adapter, View view, int pos, long id) {
int realPos = 0;
realPos = numPerPage * (currentPage - 1) + pos;
String path = resultPath.get(realPos);
final File f = new File(path);
if (f.isFile()) pickFile(f);
}
@Override
protected void onPause() {
super.onPause();
changeEinkControlPermission(false);
}
private void pickFile(File file) {
Intent intent = getIntent();
intent.setData(Uri.fromFile(file));
setResult(RESULT_OK, intent);
finish();
}
// scan all the specific type files. ex:*.note
public void searchFiles(File file) {
File[] the_Files = file.listFiles();
if (the_Files == null) return;
// search all dirs and files
for (File tempF : the_Files) {
if (tempF.isDirectory()) {
if (!tempF.isHidden()) searchFiles(tempF);
} else {
try {
boolean isImage = false;
// compare file. if the key is matched, return value > -1.
for (String type : searchImageType) {
final int index = tempF.getPath().lastIndexOf('.');
final String myExtension =
((index > 0) ? tempF.getPath().substring(index).toLowerCase().intern() : "");
if (myExtension.equals(type) && tempF.getName().substring(0, 1).equals(".") == false) {
isImage = true;
break;
} }
}
if (isImage) {
// add matched path into array
resultPath.add(tempF.getPath());
}
} catch (Exception e) {
Toast.makeText(
ImagePickerActivity.this, "error - searching image files.", Toast.LENGTH_LONG)
.show();
} }
}; }
}
}
@Override
protected void onResume() {
super.onResume();
changeEinkControlPermission(true);
// update page info
totalPage = getTotalPage();
if (currentPage == 0) currentPage = 1;
txtv_pageinfo.setText(currentPage + " of " + totalPage);
if (totalPage == 1) { // only one page
txtv_pageinfo.setVisibility(View.INVISIBLE);
imgv_pre_page.setVisibility(View.INVISIBLE);
imgv_next_page.setVisibility(View.INVISIBLE);
} else {
txtv_pageinfo.setVisibility(View.VISIBLE);
imgv_pre_page.setVisibility(View.VISIBLE);
imgv_next_page.setVisibility(View.VISIBLE);
}
}
private void changeEinkControlPermission(boolean isForNtxAppsOnly) {
Intent changePermissionIntent = new Intent("ntx.eink_control.CHANGE_PERMISSON");
changePermissionIntent.putExtra("isPermissionNtxApp", isForNtxAppsOnly);
sendBroadcast(changePermissionIntent);
}
// calculate total page
private int getTotalPage() {
int totalpage = resultPath.size() / (numPerPage);
if (resultPath.size() % (numPerPage) != 0 | resultPath.size() / (numPerPage) == 0) totalpage++;
return totalpage;
}
// update items for showing
private void updateItems() {
if (items.size() != 0) items.clear();
for (int i = numPerPage * (currentPage - 1); i < numPerPage * currentPage; i++) {
if (i >= resultPath.size()) break;
Map<String, Object> item = new HashMap<String, Object>();
item.put("image", resultPath.get(i));
items.add(item);
}
}
public class SortIgnoreCase implements Comparator<Object> {
@Override @Override
public void onItemClick(AdapterView<?> adapter, View view, int pos, long id) { public int compare(Object o1, Object o2) {
int realPos = 0; String s1 = (String) o1;
String s2 = (String) o2;
realPos = numPerPage * (currentPage - 1) + pos; final int indexNameS1 = s1.lastIndexOf('/');
String path = resultPath.get(realPos); final String filenameS1 =
final File f = new File(path); ((indexNameS1 > 1) ? s1.substring(indexNameS1).toLowerCase().intern() : "");
if (f.isFile()) final int indexNameS2 = s2.lastIndexOf('/');
pickFile(f); final String filenameS2 =
} ((indexNameS2 > 1) ? s2.substring(indexNameS2).toLowerCase().intern() : "");
return filenameS1.toLowerCase().compareTo(filenameS2.toLowerCase());
@Override
protected void onPause() {
super.onPause();
changeEinkControlPermission(false);
}
private void pickFile(File file) {
Intent intent = getIntent();
intent.setData(Uri.fromFile(file));
setResult(RESULT_OK, intent);
finish();
}
//scan all the specific type files. ex:*.note
public void searchFiles(File file) {
File[] the_Files = file.listFiles();
if (the_Files == null)
return;
//search all dirs and files
for (File tempF : the_Files) {
if (tempF.isDirectory()) {
if (!tempF.isHidden())
searchFiles(tempF);
} else {
try {
boolean isImage = false;
//compare file. if the key is matched, return value > -1.
for (String type : searchImageType) {
final int index = tempF.getPath().lastIndexOf('.');
final String myExtension = ((index > 0) ? tempF.getPath().substring(index).toLowerCase().intern() : "");
if (myExtension.equals(type) && tempF.getName().substring(0, 1).equals(".") == false) {
isImage = true;
break;
}
}
if (isImage) {
//add matched path into array
resultPath.add(tempF.getPath());
}
} catch (Exception e) {
Toast.makeText(ImagePickerActivity.this, "error - searching image files.", Toast.LENGTH_LONG).show();
}
}
}
}
@Override
protected void onResume() {
super.onResume();
changeEinkControlPermission(true);
// update page info
totalPage = getTotalPage();
if (currentPage == 0)
currentPage = 1;
txtv_pageinfo.setText(currentPage + " of " + totalPage);
if (totalPage == 1) { //only one page
txtv_pageinfo.setVisibility(View.INVISIBLE);
imgv_pre_page.setVisibility(View.INVISIBLE);
imgv_next_page.setVisibility(View.INVISIBLE);
} else {
txtv_pageinfo.setVisibility(View.VISIBLE);
imgv_pre_page.setVisibility(View.VISIBLE);
imgv_next_page.setVisibility(View.VISIBLE);
}
}
private void changeEinkControlPermission(boolean isForNtxAppsOnly) {
Intent changePermissionIntent = new Intent("ntx.eink_control.CHANGE_PERMISSON");
changePermissionIntent.putExtra("isPermissionNtxApp", isForNtxAppsOnly);
sendBroadcast(changePermissionIntent);
}
// calculate total page
private int getTotalPage() {
int totalpage = resultPath.size() / (numPerPage);
if (resultPath.size() % (numPerPage) != 0 | resultPath.size() / (numPerPage) == 0)
totalpage++;
return totalpage;
}
// update items for showing
private void updateItems() {
if (items.size() != 0)
items.clear();
for (int i = numPerPage * (currentPage - 1); i < numPerPage * currentPage; i++) {
if (i >= resultPath.size())
break;
Map<String, Object> item = new HashMap<String, Object>();
item.put("image", resultPath.get(i));
items.add(item);
}
}
public class SortIgnoreCase implements Comparator<Object> {
@Override
public int compare(Object o1, Object o2) {
String s1 = (String) o1;
String s2 = (String) o2;
final int indexNameS1 = s1.lastIndexOf('/');
final String filenameS1 = ((indexNameS1 > 1) ? s1.substring(indexNameS1).toLowerCase().intern() : "");
final int indexNameS2 = s2.lastIndexOf('/');
final String filenameS2 = ((indexNameS2 > 1) ? s2.substring(indexNameS2).toLowerCase().intern() : "");
return filenameS1.toLowerCase().compareTo(filenameS2.toLowerCase());
}
} }
}
} }

View file

@ -4,36 +4,41 @@ import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.Spinner; import android.widget.Spinner;
public class ReselectSpinner extends Spinner{ public class ReselectSpinner extends Spinner {
public ReselectSpinner(Context context) public ReselectSpinner(Context context) {
{ super(context); } super(context);
}
public ReselectSpinner(Context context, AttributeSet attrs) public ReselectSpinner(Context context, AttributeSet attrs) {
{ super(context, attrs); } super(context, attrs);
}
public ReselectSpinner(Context context, AttributeSet attrs, int defStyle) public ReselectSpinner(Context context, AttributeSet attrs, int defStyle) {
{ super(context, attrs, defStyle); } super(context, attrs, defStyle);
}
@Override public void @Override
setSelection(int position, boolean animate) public void setSelection(int position, boolean animate) {
{ boolean sameSelected = position == getSelectedItemPosition();
boolean sameSelected = position == getSelectedItemPosition(); super.setSelection(position, animate);
super.setSelection(position, animate); if (sameSelected) {
if (sameSelected) { // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it
// Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now // manually now
getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId()); getOnItemSelectedListener()
} .onItemSelected(this, getSelectedView(), position, getSelectedItemId());
} }
}
@Override public void @Override
setSelection(int position) public void setSelection(int position) {
{ boolean sameSelected = position == getSelectedItemPosition();
boolean sameSelected = position == getSelectedItemPosition(); super.setSelection(position);
super.setSelection(position); if (sameSelected) {
if (sameSelected) { // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it
// Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now // manually now
getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId()); getOnItemSelectedListener()
} .onItemSelected(this, getSelectedView(), position, getSelectedItemId());
} }
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
package ntx.demo; package ntx.demo;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
@ -7,141 +8,125 @@ import android.view.Display;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ImageView; import android.widget.ImageView;
public class NtxImageViewClass extends ImageView { public class NtxImageViewClass extends ImageView {
private static final String TAG = "ntxImageView"; private static final String TAG = "ntxImageView";
// private final Bitmap mScreen; // private final Bitmap mScreen;
private boolean mfirsttimedraw; private boolean mfirsttimedraw;
public static int FULL_REFRESH_COUNT = 600; public static int FULL_REFRESH_COUNT = 600;
/* update mode for handwriting in eink */
public static final int UPDATE_MODE_PARTIAL_DU =
android.view.View.EINK_WAVEFORM_MODE_DU | android.view.View.EINK_UPDATE_MODE_PARTIAL;
/* update mode for handwriting in eink */ public static final int UPDATE_MODE_PARTIAL_A2 =
public static final int UPDATE_MODE_PARTIAL_DU = android.view.View.EINK_WAVEFORM_MODE_A2 | android.view.View.EINK_UPDATE_MODE_PARTIAL;
android.view.View.EINK_WAVEFORM_MODE_DU
| android.view.View.EINK_UPDATE_MODE_PARTIAL;
public static final int UPDATE_MODE_PARTIAL_A2 = public static final int UPDATE_MODE_PARTIAL_GC4 =
android.view.View.EINK_WAVEFORM_MODE_A2 android.view.View.EINK_WAVEFORM_MODE_GC4 | android.view.View.EINK_UPDATE_MODE_PARTIAL;
| android.view.View.EINK_UPDATE_MODE_PARTIAL;
public static final int UPDATE_MODE_PARTIAL_GC4 = public static final int UPDATE_MODE_PARTIAL_DU_WITH_DITHER =
android.view.View.EINK_WAVEFORM_MODE_GC4 android.view.View.EINK_WAVEFORM_MODE_DU
| android.view.View.EINK_UPDATE_MODE_PARTIAL; | android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER;
public static final int UPDATE_MODE_PARTIAL_DU_WITH_DITHER = public static final int UPDATE_MODE_PARTIAL_A2_WITH_DITHER =
android.view.View.EINK_WAVEFORM_MODE_DU android.view.View.EINK_WAVEFORM_MODE_A2
| android.view.View.EINK_UPDATE_MODE_PARTIAL | android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER; | android.view.View.EINK_DITHER_MODE_DITHER;
public static final int UPDATE_MODE_PARTIAL_A2_WITH_DITHER = public static final int UPDATE_MODE_PARTIAL_GC4_WITH_DITHER =
android.view.View.EINK_WAVEFORM_MODE_A2 android.view.View.EINK_WAVEFORM_MODE_GC4
| android.view.View.EINK_UPDATE_MODE_PARTIAL | android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER; | android.view.View.EINK_DITHER_MODE_DITHER;
public static final int UPDATE_MODE_PARTIAL_GC4_WITH_DITHER = public static final int UPDATE_MODE_PARTIAL_DU_WITH_MONO =
android.view.View.EINK_WAVEFORM_MODE_GC4 android.view.View.EINK_WAVEFORM_MODE_DU
| android.view.View.EINK_UPDATE_MODE_PARTIAL | android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER; | android.view.View.EINK_DITHER_MODE_DITHER
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static final int UPDATE_MODE_PARTIAL_DU_WITH_MONO = public static final int UPDATE_MODE_PARTIAL_A2_WITH_MONO =
android.view.View.EINK_WAVEFORM_MODE_DU android.view.View.EINK_WAVEFORM_MODE_A2
| android.view.View.EINK_UPDATE_MODE_PARTIAL | android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER | android.view.View.EINK_DITHER_MODE_DITHER
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME; | android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static final int UPDATE_MODE_PARTIAL_A2_WITH_MONO = public static final int UPDATE_MODE_PARTIAL_GC4_WITH_MONO =
android.view.View.EINK_WAVEFORM_MODE_A2 android.view.View.EINK_WAVEFORM_MODE_GC4
| android.view.View.EINK_UPDATE_MODE_PARTIAL | android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER | android.view.View.EINK_DITHER_MODE_DITHER
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME; | android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static final int UPDATE_MODE_PARTIAL_GC4_WITH_MONO = public static final int UPDATE_MODE_PARTIAL_GC16 =
android.view.View.EINK_WAVEFORM_MODE_GC4 android.view.View.EINK_WAVEFORM_MODE_GC16 | android.view.View.EINK_UPDATE_MODE_PARTIAL;
| android.view.View.EINK_UPDATE_MODE_PARTIAL
| android.view.View.EINK_DITHER_MODE_DITHER
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static final int UPDATE_MODE_PARTIAL_GC16 = public static final int UPDATE_MODE_PARTIAL_AUTO =
android.view.View.EINK_WAVEFORM_MODE_GC16 android.view.View.EINK_WAVEFORM_MODE_AUTO | android.view.View.EINK_UPDATE_MODE_PARTIAL;
| android.view.View.EINK_UPDATE_MODE_PARTIAL;
public static final int UPDATE_MODE_PARTIAL_AUTO = public static final int UPDATE_MODE_FULL_GC16 =
android.view.View.EINK_WAVEFORM_MODE_AUTO android.view.View.EINK_WAVEFORM_MODE_GC16 | android.view.View.EINK_UPDATE_MODE_FULL;
| android.view.View.EINK_UPDATE_MODE_PARTIAL;
public static final int UPDATE_MODE_FULL_GC16 = public static final int UPDATE_MODE_FULL_A2 =
android.view.View.EINK_WAVEFORM_MODE_GC16 android.view.View.EINK_WAVEFORM_MODE_A2
| android.view.View.EINK_UPDATE_MODE_FULL; | android.view.View.EINK_UPDATE_MODE_FULL
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static final int UPDATE_MODE_FULL_A2 = public static final int UPDATE_MODE_FULL_DU =
android.view.View.EINK_WAVEFORM_MODE_A2 android.view.View.EINK_WAVEFORM_MODE_DU
| android.view.View.EINK_UPDATE_MODE_FULL | android.view.View.EINK_UPDATE_MODE_FULL
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME; | android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static final int UPDATE_MODE_FULL_DU = public static int UPDATE_MODE_SCREEN = UPDATE_MODE_FULL_GC16;
android.view.View.EINK_WAVEFORM_MODE_DU
| android.view.View.EINK_UPDATE_MODE_FULL
| android.view.View.EINK_MONOCHROME_MODE_MONOCHROME;
public static int UPDATE_MODE_SCREEN = UPDATE_MODE_FULL_GC16; public NtxImageViewClass(Context context) {
super(context);
mContext = context;
mfirsttimedraw = false;
}
public NtxImageViewClass(Context context) { public Bitmap loadBitmap(Drawable sprite) {
super(context); return loadBitmap(sprite, Bitmap.Config.RGB_565);
mContext = context; }
mfirsttimedraw = false;
}
public Bitmap loadBitmap(Drawable sprite) { public Bitmap loadBitmap(Drawable sprite, Bitmap.Config bitmapConfig) {
return loadBitmap(sprite, Bitmap.Config.RGB_565); Display d =
} ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int width = d.getWidth();
int height = d.getHeight();
public Bitmap loadBitmap(Drawable sprite, Bitmap.Config bitmapConfig) { Bitmap bitmap = Bitmap.createBitmap(width, height, bitmapConfig);
Display d = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)) Canvas canvas = new Canvas(bitmap);
.getDefaultDisplay(); sprite.setBounds(0, 0, width, height);
int width = d.getWidth(); sprite.draw(canvas);
int height = d.getHeight(); return bitmap;
}
Bitmap bitmap = Bitmap.createBitmap(width, height, bitmapConfig);
Canvas canvas = new Canvas(bitmap);
sprite.setBounds(0, 0, width, height);
sprite.draw(canvas);
return bitmap;
}
@Override
public void setImageDrawable(Drawable drawable) {
invalidate(UPDATE_MODE_SCREEN);
super.setImageDrawable(drawable);
}
@Override
public void setImageBitmap(Bitmap bm)
{
invalidate(UPDATE_MODE_SCREEN);
super.setImageBitmap(bm);
}
@Override
public void draw(Canvas canvas) {
if (mfirsttimedraw == false) {
invalidate(UPDATE_MODE_SCREEN);
mfirsttimedraw = true;
} else {
super.draw(canvas);
}
}
public void setUPDATE_MODE_SCREEN(int updateMode) {
UPDATE_MODE_SCREEN=updateMode;
}
@Override
public void setImageDrawable(Drawable drawable) {
invalidate(UPDATE_MODE_SCREEN);
super.setImageDrawable(drawable);
}
@Override
public void setImageBitmap(Bitmap bm) {
invalidate(UPDATE_MODE_SCREEN);
super.setImageBitmap(bm);
}
@Override
public void draw(Canvas canvas) {
if (mfirsttimedraw == false) {
invalidate(UPDATE_MODE_SCREEN);
mfirsttimedraw = true;
} else {
super.draw(canvas);
} }
}
public void setUPDATE_MODE_SCREEN(int updateMode) {
UPDATE_MODE_SCREEN = updateMode;
}
}

View file

@ -4,12 +4,6 @@ import android.graphics.Bitmap;
import android.graphics.Rect; import android.graphics.Rect;
import java.lang.reflect.Method; import java.lang.reflect.Method;
/**
* Xiaomi Mi Reader E-Ink transport via reverse-engineered EpdUtil surface. Uses reflection only (no
* vendor SDK link). Falls back gracefully when the system image denies the request. Symbol map:
* EpdUtil.sInitEBC (ctor), sRecycleEBC (close), sCommitImage (commit), sCommitColor (clear),
* sSetFastBgRefresh / sSetFastHandWrite (modes).
*/
public final class XiaomiEinkRenderer implements Renderer { public final class XiaomiEinkRenderer implements Renderer {
private static final String EPD_UTIL = "android.eink.EpdUtil"; private static final String EPD_UTIL = "android.eink.EpdUtil";
private static final String EPD_MANAGER = "android.eink.EinkManager"; private static final String EPD_MANAGER = "android.eink.EinkManager";