Skip to content

Commit 1999bef

Browse files
committed
updated code to 1.4
1 parent dc765f9 commit 1999bef

3 files changed

Lines changed: 181 additions & 54 deletions

File tree

changelog.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ Tab focus in OUYA orange instead of black-transparent
3131
1.3
3232
GridView vertically aligned
3333
Focus now scales correctly when selected (previously, the image shrank after selection)
34-
Layout improved
34+
Layout improved
35+
36+
1.4
37+
Filtering for GameStick Launcher + GameStick

sources/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ android {
77
applicationId "com.ast.ouyalaunch"
88
minSdkVersion 16
99
targetSdkVersion 34
10-
versionCode 5
11-
versionName "1.3"
10+
versionCode 6
11+
versionName "1.4"
1212
vectorDrawables.useSupportLibrary = true
1313
multiDexEnabled true
1414
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Lines changed: 175 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
package com.ast.ouyalaunch;
32

43
import android.content.Context;
@@ -9,94 +8,219 @@
98
import android.graphics.Bitmap;
109
import android.graphics.BitmapFactory;
1110
import android.util.Log;
11+
1212
import java.io.File;
1313
import java.io.FileOutputStream;
1414
import java.io.InputStream;
1515
import java.util.ArrayList;
16+
import java.util.Collections;
17+
import java.util.Comparator;
18+
import java.util.Enumeration;
19+
import java.util.HashMap;
20+
import java.util.HashSet;
1621
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.Map;
24+
import java.util.Set;
1725
import java.util.zip.ZipEntry;
1826
import java.util.zip.ZipFile;
1927

28+
/**
29+
* Scans launchable apps and builds a cache of OUYA-compatible entries, identified by the presence
30+
* of an "ouya_icon.png" inside the APK. Soft rule: if both a package and its ".launcher" twin exist,
31+
* the base package (without ".launcher") is ignored for the icon scan and only the ".launcher" app
32+
* is considered. This avoids duplicate entries and ensures the correct OUYA icon is used.
33+
*
34+
* NOTE: Keep Java 7 compatible (no lambdas).
35+
*/
2036
public class AppScanner {
2137

38+
private static final String TAG = "AppScanner";
39+
private static final String ICON_FILE_NAME = "ouya_icon.png";
40+
private static final String LAUNCHER_SUFFIX = ".launcher";
41+
42+
/** Build the full list of OUYA-compatible apps (has ouya_icon.png). */
2243
public static List<AppEntry> buildCache(Context ctx) {
23-
List<AppEntry> out = new ArrayList<>();
44+
List<AppEntry> out = new ArrayList<AppEntry>();
2445
PackageManager pm = ctx.getPackageManager();
2546

2647
try {
48+
// 1) Get all launchable activities
2749
Intent intent = new Intent(Intent.ACTION_MAIN, null);
2850
intent.addCategory(Intent.CATEGORY_LAUNCHER);
2951
List<ResolveInfo> infos = pm.queryIntentActivities(intent, 0);
52+
if (infos == null) infos = new ArrayList<ResolveInfo>();
3053

31-
if (infos == null || infos.isEmpty()) {
32-
Log.w("AppScanner", "Keine Apps gefunden.");
33-
java.util.Collections.sort(out, new java.util.Comparator<AppEntry>() {
34-
@Override
35-
public int compare(AppEntry a, AppEntry b) {
36-
return a.title.toLowerCase().compareTo(b.title.toLowerCase());
37-
}
38-
});
54+
// 2) Collect unique package names
55+
Set<String> packages = new HashSet<String>();
56+
for (int i = 0; i < infos.size(); i++) {
57+
ResolveInfo ri = infos.get(i);
58+
if (ri == null || ri.activityInfo == null) continue;
59+
String pkg = ri.activityInfo.packageName;
60+
if (pkg != null && pkg.length() > 0) {
61+
packages.add(pkg);
62+
}
63+
}
64+
65+
// 3) Compute skip set for base packages when a ".launcher" twin exists
66+
Set<String> skipBase = new HashSet<String>();
67+
for (String pkg : packages) {
68+
if (pkg.endsWith(LAUNCHER_SUFFIX)) {
69+
String base = pkg.substring(0, pkg.length() - LAUNCHER_SUFFIX.length());
70+
if (packages.contains(base)) {
71+
skipBase.add(base);
72+
}
73+
}
74+
}
75+
if (!skipBase.isEmpty()) {
76+
Log.i(TAG, "Launcher-pairs detected, will ignore bases for icon scan: " + skipBase);
77+
}
3978

40-
return out;
79+
// 4) Prepare icon cache dir
80+
File iconDir = new File(ctx.getFilesDir(), "icons");
81+
if (!iconDir.exists()) {
82+
//noinspection ResultOfMethodCallIgnored
83+
iconDir.mkdirs();
4184
}
4285

43-
for (ResolveInfo ri : infos) {
44-
String pkg = ri.activityInfo.packageName;
45-
String label = ri.loadLabel(pm).toString();
86+
// 5) Iterate packages, apply soft rule, look for ouya_icon.png, and cache
87+
for (String pkg : packages) {
88+
// Soft rule: if base is paired with ".launcher", skip base
89+
if (skipBase.contains(pkg)) {
90+
Log.d(TAG, "Skip base package due to .launcher twin: " + pkg);
91+
continue;
92+
}
4693

4794
try {
4895
ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
49-
String apkPath = ai.sourceDir;
50-
51-
boolean hasOuyaIcon = false;
52-
Bitmap bmp = null;
53-
54-
ZipFile zipFile = new ZipFile(apkPath);
55-
for (ZipEntry entry : java.util.Collections.list(zipFile.entries())) {
56-
if (entry.getName().toLowerCase().contains("drawable") &&
57-
entry.getName().toLowerCase().endsWith("ouya_icon.png")) {
58-
hasOuyaIcon = true;
59-
InputStream is = zipFile.getInputStream(entry);
60-
bmp = BitmapFactory.decodeStream(is);
61-
is.close();
62-
break;
63-
}
64-
}
65-
zipFile.close();
96+
if (ai == null || ai.sourceDir == null) continue;
6697

67-
if (!hasOuyaIcon || bmp == null) {
68-
Log.i("AppScanner", "Überspringe " + pkg + " (kein ouya_icon.png in APK)");
98+
// Search inside APK zip for ouya_icon.png (any density path)
99+
IconMatch match = findOuyaIconInApk(ai.sourceDir);
100+
if (match == null) {
101+
// No OUYA icon → not considered OUYA-compatible
69102
continue;
70103
}
71104

72-
Bitmap scaled = Bitmap.createScaledBitmap(bmp, 528, 297, true);
73-
File iconFile = new File(ctx.getFilesDir(), pkg + "_icon.png");
74-
FileOutputStream fos = new FileOutputStream(iconFile);
75-
scaled.compress(Bitmap.CompressFormat.PNG, 100, fos);
105+
// Decode and cache icon file
106+
InputStream is = match.open();
107+
if (is == null) continue;
108+
Bitmap bmp = BitmapFactory.decodeStream(is);
109+
is.close();
110+
if (bmp == null) continue;
111+
112+
File outPng = new File(iconDir, pkg + ".png");
113+
FileOutputStream fos = new FileOutputStream(outPng);
114+
bmp.compress(Bitmap.CompressFormat.PNG, 100, fos);
115+
fos.flush();
76116
fos.close();
77117

78-
AppEntry entry = new AppEntry(pkg, label, "Casual", false, iconFile.getAbsolutePath());
79-
out.add(entry);
118+
// Build entry
119+
CharSequence label = pm.getApplicationLabel(ai);
120+
String title = label != null ? label.toString() : pkg;
121+
122+
AppEntry e = new AppEntry();
123+
e.packageName = pkg;
124+
e.title = title;
125+
e.genre = "Casual"; // default; user can reassign, DataStore preserves
126+
e.favorite = false;
127+
e.iconPath = outPng.getAbsolutePath();
80128

81-
Log.i("AppScanner", "Gefunden: " + label + " (" + pkg + ")");
129+
out.add(e);
82130

83-
} catch (Throwable t) {
84-
Log.w("AppScanner", "Fehler bei " + pkg + ": " + t.getMessage());
131+
Log.d(TAG, "Added OUYA app: " + pkg + " (" + title + "), icon=" + match.entryName);
132+
} catch (Throwable perApp) {
133+
Log.w(TAG, "Failed scanning " + pkg + ": " + perApp.getMessage());
85134
}
86135
}
87-
88136
} catch (Throwable t) {
89-
Log.e("AppScanner", "Fehler beim App-Scan: " + t.getMessage(), t);
137+
Log.e(TAG, "Fehler beim App-Scan: " + t.getMessage(), t);
138+
}
139+
140+
Log.i(TAG, "Scan abgeschlossen, " + out.size() + " OUYA-kompatible Apps gefunden.");
141+
// Sort A–Z by title
142+
Collections.sort(out, new Comparator<AppEntry>() {
143+
@Override public int compare(AppEntry a, AppEntry b) {
144+
String ta = (a != null && a.title != null) ? a.title : "";
145+
String tb = (b != null && b.title != null) ? b.title : "";
146+
return ta.toLowerCase(Locale.US).compareTo(tb.toLowerCase(Locale.US));
147+
}
148+
});
149+
return out;
150+
}
151+
152+
/** Represents a lazy-open match inside the APK. */
153+
private static class IconMatch {
154+
final String apkPath;
155+
final String entryName;
156+
IconMatch(String apkPath, String entryName) { this.apkPath = apkPath; this.entryName = entryName; }
157+
InputStream open() {
158+
try {
159+
ZipFile zf = new ZipFile(apkPath);
160+
ZipEntry ze = zf.getEntry(entryName);
161+
if (ze == null) return null;
162+
// NOTE: The caller decodes & closes the stream; we keep ZipFile referenced
163+
// until stream is closed by framework.
164+
return zf.getInputStream(ze);
165+
} catch (Throwable t) {
166+
Log.w(TAG, "open() failed for " + entryName + " in " + apkPath + ": " + t.getMessage());
167+
return null;
168+
}
90169
}
170+
}
91171

92-
Log.i("AppScanner", "Scan abgeschlossen, " + out.size() + " OUYA-kompatible Apps gefunden.");
93-
java.util.Collections.sort(out, new java.util.Comparator<AppEntry>() {
94-
@Override
95-
public int compare(AppEntry a, AppEntry b) {
96-
return a.title.toLowerCase().compareTo(b.title.toLowerCase());
172+
/**
173+
* Find the best-matching "ouya_icon.png" entry within the given APK zip path.
174+
* Preference order: xxxhdpi > xxhdpi > xhdpi > hdpi > mdpi > any.
175+
*/
176+
private static IconMatch findOuyaIconInApk(String apkPath) {
177+
ZipFile zf = null;
178+
try {
179+
zf = new ZipFile(apkPath);
180+
// Collect candidates
181+
List<String> candidates = new ArrayList<String>();
182+
Enumeration<? extends ZipEntry> en = zf.entries();
183+
while (en.hasMoreElements()) {
184+
ZipEntry ze = en.nextElement();
185+
if (ze == null) continue;
186+
String name = ze.getName();
187+
if (name == null) continue;
188+
String lower = name.toLowerCase(Locale.US);
189+
if (lower.endsWith("/" + ICON_FILE_NAME) || lower.equals(ICON_FILE_NAME)) {
190+
candidates.add(name);
191+
}
192+
}
193+
if (candidates.isEmpty()) return null;
194+
195+
// Rank by density hints if available
196+
int bestScore = -1;
197+
String best = null;
198+
for (int i = 0; i < candidates.size(); i++) {
199+
String n = candidates.get(i);
200+
String ln = n.toLowerCase(Locale.US);
201+
int score = densityScore(ln);
202+
if (score > bestScore) {
203+
bestScore = score;
204+
best = n;
205+
}
206+
}
207+
if (best == null) best = candidates.get(0);
208+
// Don't keep the ZipFile open; IconMatch#open will reopen on demand
209+
zf.close();
210+
return new IconMatch(apkPath, best);
211+
} catch (Throwable t) {
212+
try { if (zf != null) zf.close(); } catch (Throwable ignore) {}
213+
return null;
214+
}
97215
}
98-
});
99216

100-
return out;
217+
private static int densityScore(String pathLower) {
218+
// Higher score = higher preference
219+
if (pathLower.contains("xxxhdpi")) return 5;
220+
if (pathLower.contains("xxhdpi")) return 4;
221+
if (pathLower.contains("xhdpi")) return 3;
222+
if (pathLower.contains("hdpi")) return 2;
223+
if (pathLower.contains("mdpi")) return 1;
224+
return 0;
101225
}
102226
}

0 commit comments

Comments
 (0)