Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/temp-quest-keepawake-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Temp Quest KeepAwake APK
on:
pull_request:
branches: [main]
paths:
- 'quest-keepawake-build/**'
- '.github/workflows/temp-quest-keepawake-build.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v3
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '8.4'
- name: Install SDK 33
run: sdkmanager 'platforms;android-33' 'build-tools;33.0.2'
- name: Build debug APK
run: gradle -p quest-keepawake-build assembleDebug --stacktrace
- uses: actions/upload-artifact@v4
with:
name: QuestNavKeepAwake-always-on-debug
path: quest-keepawake-build/app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error
9 changes: 9 additions & 0 deletions quest-keepawake-build/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins { id 'com.android.application' }
android {
namespace 'com.questnav.keepawake'
compileSdk 33
defaultConfig { applicationId 'com.questnav.keepawake'; minSdk 29; targetSdk 32; versionCode 2; versionName '1.1-always-on' }
buildTypes { release { minifyEnabled false }; debug { minifyEnabled false } }
compileOptions { sourceCompatibility JavaVersion.VERSION_17; targetCompatibility JavaVersion.VERSION_17 }
lint { abortOnError false }
}
17 changes: 17 additions & 0 deletions quest-keepawake-build/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application android:label="Quest KeepAwake" android:theme="@android:style/Theme.DeviceDefault">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity>
<receiver android:name=".BootReceiver" android:exported="true" android:directBootAware="true">
<intent-filter><action android:name="android.intent.action.BOOT_COMPLETED"/><action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/></intent-filter>
</receiver>
<service android:name=".KeepAwakeService" android:exported="false" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

BootReceiverandroid:directBootAware="true" が設定されており、デバイスの起動直後(ロック解除前)に送信される LOCKED_BOOT_COMPLETED を受信した際に KeepAwakeService を起動しようとします。
しかし、起動対象の KeepAwakeService 自体に android:directBootAware="true" が設定されていないため、ロック解除前にサービスを起動しようとするとシステムによって起動が拒否されるか、正常に動作しません。

デバイスの起動直後から確実にサービスを動作させるために、KeepAwakeService にも android:directBootAware="true" を指定することをお勧めします。

Suggested change
<service android:name=".KeepAwakeService" android:exported="false" />
<service android:name=".KeepAwakeService" android:exported="false" android:directBootAware="true" />

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.questnav.keepawake;
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent;
public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context c, Intent i){ c.startForegroundService(new Intent(c, KeepAwakeService.class)); } }
Comment on lines +2 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Android 12 (API 31) 以降では、バックグラウンドからフォアグラウンドサービスを開始する際、特定の条件下で ForegroundServiceStartNotAllowedExceptionIllegalStateException のサブクラス)がスローされる可能性があります。
ブロードキャストレシーバー内での未キャッチの例外はアプリのクラッシュ(「アプリが繰り返し停止しています」など)を引き起こすため、startForegroundService の呼び出しを try-catch ブロックで囲み、安全に例外を処理することをお勧めします。

Suggested change
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent;
public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context c, Intent i){ c.startForegroundService(new Intent(c, KeepAwakeService.class)); } }
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context c, Intent i) {
try {
c.startForegroundService(new Intent(c, KeepAwakeService.class));
} catch (Exception e) {
Log.e("BootReceiver", "Failed to start KeepAwakeService on boot", e);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.questnav.keepawake;
import android.app.*; import android.content.*; import android.os.*; import android.provider.Settings; import android.util.Log;
public class KeepAwakeService extends Service {
private static final String TAG="KeepAwakeService", CHANNEL_ID="keepawake_channel"; private static final long INTERVAL_MS=2000;
private final Handler handler=new Handler(Looper.getMainLooper()); private boolean running; private PowerManager.WakeLock wakeLock;
private final Runnable keepAwakeRunnable=new Runnable(){ @Override public void run(){ if(!running)return; sendProxClose(); handler.postDelayed(this,INTERVAL_MS); }};
@Override public void onCreate(){ super.onCreate(); }
@Override public int onStartCommand(Intent intent,int flags,int startId){ if(running)return START_STICKY; running=true; createChannel(); startForeground(1,new Notification.Builder(this,CHANNEL_ID).setContentTitle("Quest KeepAwake").setContentText("Always-on keep-awake is active").setSmallIcon(android.R.drawable.ic_lock_idle_lock).setOngoing(true).build()); PowerManager pm=(PowerManager)getSystemService(POWER_SERVICE); wakeLock=pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"QuestNavKeepAwake::StayAwake"); wakeLock.acquire(); applySettings(); sendProxClose(); handler.postDelayed(keepAwakeRunnable,INTERVAL_MS); Log.i(TAG,"Always-on service started"); return START_STICKY; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

防御的プログラミングの観点から、getSystemService(POWER_SERVICE)null を返した場合に NullPointerException が発生するのを防ぐため、PowerManager のヌルチェックを行うことをお勧めします。また、コードの可読性向上のために適切に改行とインデントを適用しています。

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (running) return START_STICKY;
        running = true;
        createChannel();
        startForeground(1, new Notification.Builder(this, CHANNEL_ID)
                .setContentTitle("Quest KeepAwake")
                .setContentText("Always-on keep-awake is active")
                .setSmallIcon(android.R.drawable.ic_lock_idle_lock)
                .setOngoing(true)
                .build());
        PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
        if (pm != null) {
            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "QuestNavKeepAwake::StayAwake");
            wakeLock.acquire();
        }
        applySettings();
        sendProxClose();
        handler.postDelayed(keepAwakeRunnable, INTERVAL_MS);
        Log.i(TAG, "Always-on service started");
        return START_STICKY;
    }

@Override public void onDestroy(){ running=false; handler.removeCallbacks(keepAwakeRunnable); if(wakeLock!=null&&wakeLock.isHeld())wakeLock.release(); super.onDestroy(); }
@Override public IBinder onBind(Intent intent){ return null; }
private void applySettings(){ ContentResolver cr=getContentResolver(); try{Settings.System.putInt(cr,Settings.System.SCREEN_OFF_TIMEOUT,Integer.MAX_VALUE);}catch(Exception e){Log.w(TAG,"screen timeout",e);} try{Settings.Global.putInt(cr,Settings.Global.STAY_ON_WHILE_PLUGGED_IN,3);}catch(Exception e){Log.w(TAG,"stay on",e);} try{Settings.Secure.putInt(cr,"adaptive_sleep",-1); Settings.Secure.putInt(cr,"sleep_timeout",-1); Settings.Secure.putInt(cr,"wake_gesture_enabled",0);}catch(Exception e){Log.w(TAG,"secure settings",e);} }
private void sendProxClose(){ try{Intent i=new Intent("com.oculus.vrpowermanager.prox_close"); i.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); sendBroadcast(i); Log.d(TAG,"Sent prox_close");}catch(Exception e){Log.w(TAG,"prox_close",e);} }
private void createChannel(){ NotificationChannel c=new NotificationChannel(CHANNEL_ID,"KeepAwake Service",NotificationManager.IMPORTANCE_LOW); getSystemService(NotificationManager.class).createNotificationChannel(c); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.questnav.keepawake;
import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.widget.TextView;
public class MainActivity extends Activity {
@Override public void onCreate(Bundle b){ super.onCreate(b); startForegroundService(new Intent(this, KeepAwakeService.class)); TextView v=new TextView(this); v.setText("KeepAwake service is running.\nprox_close is sent every 2 seconds."); v.setTextSize(20); v.setPadding(32,32,32,32); setContentView(v); }
Comment on lines +2 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Android 6.0 (API 23) 以降では、Settings.System への書き込み(SCREEN_OFF_TIMEOUT の変更など)を行うために、マニフェストでの宣言に加えて、実行時にユーザーから「システム設定の変更」権限(WRITE_SETTINGS)を明示的に許可してもらう必要があります。
現在、権限がない状態で Settings.System.putInt を呼び出すと SecurityException が発生し、try-catch でキャッチされてサイレントに失敗します(画面消灯時間が変更されません)。

MainActivity の起動時などに Settings.System.canWrite(this) をチェックし、許可されていない場合は設定画面を開いてユーザーに許可を求めるように改善することをお勧めします。

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.widget.TextView;

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle b) {
        super.onCreate(b);
        if (!Settings.System.canWrite(this)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
            intent.setData(Uri.parse("package:" + getPackageName()));
            startActivity(intent);
        }
        startForegroundService(new Intent(this, KeepAwakeService.class));
        TextView v = new TextView(this);
        v.setText("KeepAwake service is running.\\nprox_close is sent every 2 seconds.");
        v.setTextSize(20);
        v.setPadding(32, 32, 32, 32);
        setContentView(v);
    }

}
3 changes: 3 additions & 0 deletions quest-keepawake-build/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id 'com.android.application' version '8.1.4' apply false
}
2 changes: 2 additions & 0 deletions quest-keepawake-build/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
4 changes: 4 additions & 0 deletions quest-keepawake-build/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS); repositories { google(); mavenCentral() } }
rootProject.name = 'QuestNavKeepAwake'
include ':app'
Loading