Support to sort by most recent usage
This commit is contained in:
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -168,8 +179,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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user