Merge pull request #19 from tverona1/misc

Support for sorting by "most recent", some fixes
This commit is contained in:
tverona1
2019-08-24 17:50:18 -07:00
committed by GitHub
11 changed files with 2115 additions and 40 deletions
@@ -0,0 +1,84 @@
fileFormatVersion: 2
guid: 6fd132ea4bdd3ec4298a583f001ed5ab
timeCreated: 1513127630
licenseType: Store
TextureImporter:
fileIDToRecycleName: {}
serializedVersion: 4
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 0
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
filterMode: -1
aniso: -1
mipBias: -1
wrapMode: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spritePixelsToUnits: 100
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
- buildTarget: Standalone
maxTextureSize: 2048
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
- buildTarget: Android
maxTextureSize: 2048
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
spritePackingTag:
userData:
assetBundleName:
assetBundleVariant:
@@ -11,4 +11,5 @@
</activity>
</application>
<uses-feature android:name="android.hardware.vr.headtracking" android:required="false" android:version="1" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
</manifest>
+62 -7
View File
@@ -2,17 +2,25 @@ package aaa.QuestAppLauncher.App;
import com.unity3d.player.UnityPlayerActivity;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.ApplicationInfo;
import android.provider.Settings;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Calendar;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.os.Bundle;
@@ -22,23 +30,30 @@ import android.graphics.drawable.BitmapDrawable;
import java.util.List;
import java.util.LinkedList;
class AppInfoInternal {
public ApplicationInfo app;
public long lastTimeUsed;
}
public class AppInfo extends UnityPlayerActivity {
private static final String TAG = "AppInfo";
private List<ApplicationInfo> installedApps;
private List<AppInfoInternal> installedApps;
@Override
protected void onStart() {
super.onStart();
installedApps = new LinkedList<ApplicationInfo>();
installedApps = new LinkedList<AppInfoInternal>();
for(ApplicationInfo app : this.getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA)) {
if((app.flags & (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP | ApplicationInfo.FLAG_SYSTEM)) > 0) {
// Skip system app
continue;
}
installedApps.add(app);
AppInfoInternal appInfoInternal = new AppInfoInternal();
appInfoInternal.app = app;
installedApps.add(appInfoInternal);
}
}
@@ -47,11 +62,16 @@ public class AppInfo extends UnityPlayerActivity {
}
public String getPackageName(int i) {
return this.installedApps.get(i).packageName;
return this.installedApps.get(i).app.packageName;
}
public String getAppName(int i) {
return (String)this.getPackageManager().getApplicationLabel(installedApps.get(i));
return (String)this.getPackageManager().getApplicationLabel(installedApps.get(i).app);
}
public long getLastTimeUsed(int i)
{
return this.installedApps.get(i).lastTimeUsed;
}
public boolean isQuestApp(int i) {
@@ -73,7 +93,7 @@ public class AppInfo extends UnityPlayerActivity {
public boolean is2DApp(int i)
{
ApplicationInfo app = this.installedApps.get(i);
ApplicationInfo app = this.installedApps.get(i).app;
if (null == app.metaData)
{
return true;
@@ -88,7 +108,7 @@ public class AppInfo extends UnityPlayerActivity {
}
public byte[] getIcon(int i) {
BitmapDrawable icon = (BitmapDrawable)this.getPackageManager().getApplicationIcon(installedApps.get(i));
BitmapDrawable icon = (BitmapDrawable)this.getPackageManager().getApplicationIcon(installedApps.get(i).app);
Bitmap bmp = icon.getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 100, stream);
@@ -96,6 +116,41 @@ public class AppInfo extends UnityPlayerActivity {
return byteArray;
}
public boolean hasUsageStatsPermissions() {
AppOpsManager appOps = (AppOpsManager) this.getSystemService(Context.APP_OPS_SERVICE);
final int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), this.getPackageName());
boolean granted = mode == AppOpsManager.MODE_DEFAULT ?
(this.checkCallingOrSelfPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED)
: (mode == AppOpsManager.MODE_ALLOWED);
return granted;
}
public void grantUsageStatsPermission() {
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
}
public void processLastTimeUsed(int numDaysLookback) {
if (!hasUsageStatsPermissions()) {
Log.i(TAG, "PorcessLastTimeUsed: No permissions, so skipping");
}
UsageStatsManager usageStatsManager = (UsageStatsManager) this.getSystemService(Context.USAGE_STATS_SERVICE);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -1 * numDaysLookback);
long start = calendar.getTimeInMillis();
long end = System.currentTimeMillis();
Map<String, UsageStats> stats = usageStatsManager.queryAndAggregateUsageStats(start, end);
for (int i = 0; i < this.installedApps.size(); i++) {
if (stats.containsKey(getPackageName(i))) {
AppInfoInternal app = this.installedApps.get(i);
app.lastTimeUsed = stats.get(getPackageName(i)).getLastTimeStamp();
Log.v(TAG, "Package " + getPackageName(i) + " last time stamp = " + app.lastTimeUsed);
this.installedApps.set(i, app);
}
}
}
public static void unzip(String zipFileName, String targetPath) {
File outDir = new File(targetPath);
File diff suppressed because it is too large Load Diff
+17 -1
View File
@@ -23,6 +23,7 @@ namespace QuestAppLauncher
public string Tab1Name;
public string Tab2Name;
public string IconPath;
public long LastTimeUsed;
}
public class AppProcessor
@@ -60,6 +61,9 @@ namespace QuestAppLauncher
public const string Tab_2D = "2D";
public const string Tab_All = "All";
// LastUsage lookback days
const int LastUsedLookbackDays = 30;
public static readonly string[] Auto_Tabs = { Tab_Quest, Tab_Go, Tab_2D };
public static Dictionary<string, ProcessedApp> ProcessApps(Config config)
@@ -79,6 +83,14 @@ namespace QuestAppLauncher
int numApps = currentActivity.Call<int>("getSize");
Debug.Log("# installed apps: " + numApps);
var processedLastTimeUsed = false;
if (config.sortMode.Equals(Config.Sort_MostRecent, StringComparison.OrdinalIgnoreCase))
{
// Process last time used for each app
currentActivity.Call("processLastTimeUsed", LastUsedLookbackDays);
processedLastTimeUsed = true;
}
// Add current package name to excludedPackageNames to filter it out
excludedPackageNames.Add(currentActivity.Call<string>("getPackageName"));
@@ -102,6 +114,7 @@ namespace QuestAppLauncher
{
var packageName = currentActivity.Call<string>("getPackageName", i);
var appName = currentActivity.Call<string>("getAppName", i);
var lastTimeUsed = processedLastTimeUsed ? currentActivity.Call<long>("getLastTimeUsed", i) : 0;
if (excludedPackageNames.Contains(packageName))
{
@@ -132,7 +145,8 @@ namespace QuestAppLauncher
tabName = Tab_Go;
}
apps.Add(packageName, new ProcessedApp { PackageName = packageName, Index = i, AutoTabName = tabName, AppName = appName });
apps.Add(packageName, new ProcessedApp { PackageName = packageName, Index = i,
AutoTabName = tabName, AppName = appName, LastTimeUsed = lastTimeUsed });
Debug.LogFormat("[{0}] package: {1}, name: {2}, auto tab: {3}", i, packageName, appName, tabName);
}
@@ -239,6 +253,7 @@ namespace QuestAppLauncher
AutoTabName = autoTabName ?? apps[entry.Key].AutoTabName,
Tab1Name = tab1 ?? apps[entry.Key].Tab1Name,
Tab2Name = tab2 ?? apps[entry.Key].Tab2Name,
LastTimeUsed = apps[entry.Key].LastTimeUsed
};
}
}
@@ -326,6 +341,7 @@ namespace QuestAppLauncher
AutoTabName = autoTabName ?? apps[entry[0]].AutoTabName,
Tab1Name = tab1 ?? apps[entry[0]].Tab1Name,
Tab2Name = tab2 ?? apps[entry[0]].Tab2Name,
LastTimeUsed = apps[entry[0]].LastTimeUsed
};
}
}
+31 -4
View File
@@ -25,6 +25,9 @@ namespace QuestAppLauncher
// Manifest file to track what we've downloaded
const string DownloadManifestFile = "download_manifest.json";
// Temporary filename for download
const string TempDownloadFileExtention = ".tmp_download";
// GitHub API url
const string GithubApiUrl = @"http://api.github.com/repos/";
@@ -179,7 +182,9 @@ namespace QuestAppLauncher
{
// Get asset info from repos
var assetsInfo = new Dictionary<string, AssetInfo>(StringComparer.OrdinalIgnoreCase);
var reposLoaded = new HashSet<string>();
// Get the set of repo URIs (removing any duplicates)
var configRepos = new HashSet<string>();
foreach (var item in config.downloadRepos)
{
if (null == item.type || !string.Equals(item.type, Config.DownloadRepo_Type_GitHub, StringComparison.OrdinalIgnoreCase))
@@ -188,11 +193,17 @@ namespace QuestAppLauncher
continue;
}
configRepos.Add(item.repoUri);
}
var reposLoaded = new HashSet<string>();
foreach (var repoUri in configRepos)
{
// Get assets from the GitHub repo
var repoLoaded = await GetAssetsInfoFromGithubRepoAsync(item.repoUri, assetsInfo, downloadProgress);
var repoLoaded = await GetAssetsInfoFromGithubRepoAsync(repoUri, assetsInfo, downloadProgress);
if (repoLoaded)
{
reposLoaded.Add(item.repoUri);
reposLoaded.Add(repoUri);
}
}
@@ -368,6 +379,7 @@ namespace QuestAppLauncher
IDownloadProgress downloadProgress)
{
var filePath = Path.Combine(GetOrCreateDownloadPath(), name);
var tempFilePath = filePath + TempDownloadFileExtention;
Debug.LogFormat("Downloading asset {0} from {1}", filePath, assetInfo.Url);
try
@@ -380,7 +392,7 @@ namespace QuestAppLauncher
{
downloadProgress.OnDownloadStart(name);
}
var downloadHandler = new DownloadHandlerFileWithProgress(filePath, downloadProgress.OnDownloadProgress);
var downloadHandler = new DownloadHandlerFileWithProgress(tempFilePath, downloadProgress.OnDownloadProgress);
downloadHandler.removeFileOnAbort = true;
req.downloadHandler = downloadHandler;
await req.SendWebRequest();
@@ -394,9 +406,24 @@ namespace QuestAppLauncher
downloadProgress.OnError(string.Format("Error updating: {0} ({1})",
req.error, assetInfo.Url));
}
if (File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
return false;
}
// Rename temp file to desination file to ensure that we are not downloading
// an error body message into the destination file.
if (File.Exists(filePath))
{
File.Delete(filePath);
}
File.Move(tempFilePath, filePath);
if (null != downloadProgress)
{
downloadProgress.OnDownloadFinish();
+19 -4
View File
@@ -20,6 +20,10 @@ namespace QuestAppLauncher
public const string Category_Left = "left";
public const string Category_Right = "right";
// Sort settings
public const string Sort_AZ = "az";
public const string Sort_MostRecent = "mostRecent";
// Download repos
public const string DownloadRepo_Type_GitHub = "github";
public const string DownloadRepo_Default = @"tverona1/QuestAppLauncher_Assets/releases/latest";
@@ -47,6 +51,9 @@ namespace QuestAppLauncher
// Grid size, specified as cols x rows
public GridSize gridSize = new GridSize();
// Sort mode
public string sortMode = Sort_AZ;
// Whether to show 2D apps
public bool show2D = false;
@@ -60,10 +67,18 @@ namespace QuestAppLauncher
public bool autoUpdate = false;
// Github download repos
public List<DownloadRepo> downloadRepos = new List<DownloadRepo>()
public List<DownloadRepo> downloadRepos = new List<DownloadRepo>();
public Config(bool initDefaults = false)
{
new DownloadRepo { repoUri = DownloadRepo_Default, type = DownloadRepo_Type_GitHub }
};
if (initDefaults)
{
// We must initialize any default collection values here. Otherwise, if we initialize them inline,
// we'll keep adding duplicate values whenever we persist via JSON.NET (since it invokes the default contructor as part
// of deserialization, which again adds the default value).
this.downloadRepos.Add(new DownloadRepo { repoUri = DownloadRepo_Default, type = DownloadRepo_Type_GitHub });
}
}
}
/// <summary>
@@ -102,7 +117,7 @@ namespace QuestAppLauncher
}
// Return default config
return new Config();
return new Config(true);
}
/// <summary>
+21 -5
View File
@@ -17,6 +17,17 @@ namespace QuestAppLauncher
/// </summary>
public class GridPopulation : MonoBehaviour
{
public class AppComparer : IComparer<ProcessedApp>
{
public int Compare(ProcessedApp x, ProcessedApp y)
{
// Order by last used and then alphabetical to break ties
return (x.LastTimeUsed != y.LastTimeUsed) ?
(y.LastTimeUsed - x.LastTimeUsed > 0 ? 1 : -1) :
string.Compare(x.AppName, y.AppName, true);
}
}
// Grid container game object
public GameObject panelContainer;
@@ -114,17 +125,22 @@ namespace QuestAppLauncher
var rightTabs = new List<string>();
// Set auto tabs
var autoTabs = AppProcessor.Auto_Tabs.Intersect(
apps.Where(x => null != x.Value.AutoTabName).Select(x => x.Value.AutoTabName)
.Distinct(StringComparer.CurrentCultureIgnoreCase).ToList(),
StringComparer.CurrentCultureIgnoreCase);
if (config.autoCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase))
{
topTabs.AddRange(AppProcessor.Auto_Tabs);
topTabs.AddRange(autoTabs);
}
else if (config.autoCategory.Equals(Config.Category_Left, StringComparison.OrdinalIgnoreCase))
{
leftTabs.AddRange(AppProcessor.Auto_Tabs);
leftTabs.AddRange(autoTabs);
}
else if (config.autoCategory.Equals(Config.Category_Right, StringComparison.OrdinalIgnoreCase))
{
rightTabs.AddRange(AppProcessor.Auto_Tabs);
rightTabs.AddRange(autoTabs);
}
// Set custom tabs, sorted alphabetically
@@ -168,8 +184,8 @@ namespace QuestAppLauncher
}
// Populate grid with app information (name & icon)
// Sort by app name
foreach (var app in apps.OrderBy(key => key.Value.AppName))
// Sort by custom comparer
foreach (var app in apps.OrderBy(key => key.Value, new AppComparer()))
{
// Add to all tab
await AddCellToGridAsync(app.Value, gridContents[AppProcessor.Tab_All].transform);
+84
View File
@@ -33,6 +33,11 @@ namespace QuestAppLauncher
public Toggle tabsCustomLeft;
public Toggle tabsCustomRight;
public Toggle sortAZ;
public Toggle sortMostRecent;
public GameObject usageStatsPermText;
private bool deletedHiddenAppsFile = false;
private Config config = null;
@@ -60,6 +65,9 @@ namespace QuestAppLauncher
var rowsText = this.gridRowsText.GetComponent<TextMeshProUGUI>();
rowsText.text = string.Format("{0} Rows", this.config.gridSize.rows);
// initialize sort mode
InitializeSortMode();
// Set 2D toggle
this.show2DToggle.GetComponent<Toggle>().SetIsOnWithoutNotify(this.config.show2D);
@@ -144,6 +152,65 @@ namespace QuestAppLauncher
rowsText.text = string.Format("{0} Rows", rows);
}
private bool HasUsageStatsPermissions()
{
// Check if we have UsageStats permission
using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (AndroidJavaObject currentActivity = unity.GetStatic<AndroidJavaObject>("currentActivity"))
{
var hasUsageStatsPermissions = currentActivity.Call<bool>("hasUsageStatsPermissions");
Debug.LogFormat("UsageStatsPermission: {0}", hasUsageStatsPermissions);
return hasUsageStatsPermissions;
}
}
private void InitializeSortMode()
{
bool hasUsageStatsPermissions = HasUsageStatsPermissions();
// Indicate whether we need to get permission
this.usageStatsPermText.SetActive(!hasUsageStatsPermissions);
if (hasUsageStatsPermissions &&
this.config.sortMode.Equals(Config.Sort_MostRecent, StringComparison.OrdinalIgnoreCase))
{
// Have UsageStats permission, so set it to on
this.sortMostRecent.isOn = true;
}
else
{
// Default is to sort by AZ
this.sortAZ.isOn = true;
}
this.sortMostRecent.onValueChanged.AddListener((bool isOn) => {
if (isOn)
{
// Re-check permissions
bool hasPerms = HasUsageStatsPermissions();
// Indicate whether we need to get permission
this.usageStatsPermText.SetActive(!hasPerms);
if (!hasPerms)
{
this.sortMostRecent.SetIsOnWithoutNotify(false);
this.sortAZ.SetIsOnWithoutNotify(true);
// Ask for permissions
using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (AndroidJavaObject currentActivity = unity.GetStatic<AndroidJavaObject>("currentActivity"))
{
currentActivity.Call("grantUsageStatsPermission");
// Quest doesn't like multiple apps running - kill ourself
UnityEngine.Application.Quit();
}
}
}
});
}
private void PersistConfig()
{
bool saveConfig = false;
@@ -160,6 +227,23 @@ namespace QuestAppLauncher
saveConfig = true;
}
// Update sort mode
string sortMode;
if (this.sortMostRecent.isOn)
{
sortMode = Config.Sort_MostRecent;
}
else
{
sortMode = Config.Sort_AZ;
}
if (!this.config.sortMode.Equals(sortMode, StringComparison.OrdinalIgnoreCase))
{
this.config.sortMode = sortMode;
saveConfig = true;
}
// Update 2D toggle
var show2D = this.show2DToggle.GetComponent<Toggle>().isOn;
if (show2D != this.config.show2D)
+1 -1
View File
@@ -120,7 +120,7 @@ PlayerSettings:
16:10: 1
16:9: 1
Others: 1
bundleVersion: 0.6
bundleVersion: 0.9
preloadedAssets: []
metroInputSource: 0
wsaTransparentSwapchain: 0