Repository: LawnchairLauncher/lawnfeed
Branch: master
Commit: 39ab76bad5d7
Files: 34
Total size: 57.5 KB
Directory structure:
gitextract_4wvmvqnb/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .gitmodules
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── aidl/
│ │ └── amirz/
│ │ └── aidlbridge/
│ │ ├── IBridge.aidl
│ │ └── IBridgeCallback.aidl
│ ├── java/
│ │ ├── amirz/
│ │ │ └── aidlbridge/
│ │ │ ├── BridgeImpl.java
│ │ │ ├── BridgeService.java
│ │ │ └── TransactProxy.java
│ │ └── app/
│ │ └── lawnchair/
│ │ └── lawnfeed/
│ │ ├── LauncherClientProxyService.kt
│ │ ├── PermissionActivity.java
│ │ ├── ProxyImpl.kt
│ │ ├── bridge/
│ │ │ └── TransactProxy.kt
│ │ ├── receivers/
│ │ │ ├── DownloadReceiver.java
│ │ │ └── UpdateReceiver.java
│ │ └── updater/
│ │ ├── Updater.java
│ │ └── UpdaterTask.java
│ └── res/
│ ├── drawable/
│ │ └── ic_lawnchair.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── xml/
│ └── file_paths.xml
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: Build signed debug APK
on:
workflow_dispatch:
push:
branches:
- master
jobs:
build-signed-debug-apk:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Check out repository
uses: actions/checkout@v2.3.4
with:
submodules: true
- name: Restore Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Set up Java 8
uses: actions/setup-java@v1.4.3
with:
java-version: 8
- name: Grant execution permission to Gradle Wrapper
run: chmod +x gradlew
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Setup build tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- name: Sign debug APK
uses: r0adkll/sign-android-release@v1
id: sign-debug-apk
with:
releaseDirectory: app/build/outputs/apk/debug
signingKeyBase64: ${{ secrets.KEYSTORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Signed Debug APK
path: ${{ steps.sign-debug-apk.outputs.signedReleaseFile }}
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild
.idea
================================================
FILE: .gitmodules
================================================
[submodule "launcherclient"]
path = launcherclient
url = https://github.com/LawnchairLauncher/launcherclient.git
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
final def commitHash = { ->
final def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short=7', 'HEAD'
standardOutput = stdout
}
stdout.toString().trim()
}
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "app.lawnchair.lawnfeed"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "4.0+${commitHash()}"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "Lawnfeed ${variant.versionName}.apk"
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.core:core:1.0.2"
implementation('com.googlecode.json-simple:json-simple:1.1.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
implementation project(':launcherclient')
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Users\papho\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.lawnchair.lawnfeed">
<!-- Let's break some rules... -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<permission
android:name="app.lawnchair.lawnfeed.CONNECT_SERVICE"
android:label="Connect to Lawnfeed service"
android:permissionGroup="app.lawnchair.lawnfeed.CONNECT_SERVICE"
android:protectionLevel="signature" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".LauncherClientProxyService"
android:enabled="true"
android:exported="true" />
<service android:name="amirz.aidlbridge.BridgeService">
<intent-filter>
<action android:name="com.android.launcher3.WINDOW_OVERLAY" />
<data android:scheme="app" />
</intent-filter>
</service>
<!-- Required for Lawnfeed updater -->
<receiver
android:name=".receivers.UpdateReceiver" />
<activity android:name=".PermissionActivity"
android:theme="@style/Theme.Transparent"
android:autoRemoveFromRecents="true"
android:launchMode="singleTask"
android:alwaysRetainTaskState="false">
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="app.lawnchair.lawnfeed.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
================================================
FILE: app/src/main/aidl/amirz/aidlbridge/IBridge.aidl
================================================
package amirz.aidlbridge;
import amirz.aidlbridge.IBridgeCallback;
interface IBridge {
oneway void bindService(in IBridgeCallback cb, in int flags);
}
================================================
FILE: app/src/main/aidl/amirz/aidlbridge/IBridgeCallback.aidl
================================================
package amirz.aidlbridge;
interface IBridgeCallback {
oneway void onServiceConnected(in ComponentName name, in IBinder service);
oneway void onServiceDisconnected(in ComponentName name);
}
================================================
FILE: app/src/main/java/amirz/aidlbridge/BridgeImpl.java
================================================
package amirz.aidlbridge;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;
import app.lawnchair.lawnfeed.bridge.TransactProxy;
class BridgeImpl extends IBridge.Stub {
private static final String TAG = "BridgeImpl";
private final Context mContext;
private final String mPackage;
private final Intent mIntent;
private final Set<ServiceConnection> mConnections = new HashSet<>();
BridgeImpl(Context context, Intent intent) {
mContext = context;
Uri caller = intent.getData();
String authority = caller.getEncodedAuthority();
mPackage = TextUtils.isEmpty(authority) ? "" : authority.split(":")[0];
mIntent = intent.cloneFilter();
mIntent.setPackage("com.google.android.googlequicksearchbox");
String auth = context.getPackageName() + ":" + Process.myUid();
mIntent.setData(caller.buildUpon().encodedAuthority(auth).build());
}
String getPackage() {
return mPackage;
}
@Override
public void bindService(final IBridgeCallback cb, int flags) {
Log.e(TAG, "Connect request from " + getPackage());
ServiceConnection connection = new ServiceConnection() {
private boolean mConnected;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (!mConnected) {
mConnected = true;
Log.e(TAG, "Connected for " + mPackage);
try {
cb.onServiceConnected(name, new TransactProxy(service, mContext));
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mConnected) {
mConnected = false;
Log.e(TAG, "Disconnected for " + mPackage);
try {
cb.onServiceDisconnected(name);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
};
if (mContext.bindService(mIntent, connection, flags)) {
mConnections.add(connection);
}
}
void disconnect() {
Log.e(TAG, "Disconnect " + mPackage + " with " + mConnections.size() + " connections");
for (ServiceConnection connection : mConnections) {
mContext.unbindService(connection);
connection.onServiceDisconnected(null);
}
mConnections.clear();
}
}
================================================
FILE: app/src/main/java/amirz/aidlbridge/BridgeService.java
================================================
package amirz.aidlbridge;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
public class BridgeService extends Service {
private static final String TAG = "BridgeService";
private static String sLastConnection;
private final Map<Intent, BridgeImpl> mBridges = new HashMap<>();
@Override
public IBinder onBind(Intent intent) {
Uri caller = intent.getData();
if (caller != null) {
Log.e(TAG, "Bind from " + caller.toString());
if (!mBridges.containsKey(intent)) {
mBridges.put(intent, new BridgeImpl(getApplicationContext(), intent) {
@Override
public void bindService(final IBridgeCallback cb, int flags) {
sLastConnection = getPackage();
for (BridgeImpl bridge : mBridges.values()) {
if (bridge != this) {
bridge.disconnect();
}
}
super.bindService(cb, flags);
}
});
}
return mBridges.get(intent);
}
return null;
}
@Override
public boolean onUnbind(Intent intent) {
Uri caller = intent.getData();
if (caller != null) {
Log.e(TAG, "Unbind from " + caller.toString());
if (mBridges.containsKey(intent)) {
mBridges.remove(intent).disconnect();
}
}
return super.onUnbind(intent);
}
public static String getLastConnection() {
return sLastConnection;
}
}
================================================
FILE: app/src/main/java/amirz/aidlbridge/TransactProxy.java
================================================
package amirz.aidlbridge;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
class TransactProxy extends Binder {
private final IBinder mTarget;
TransactProxy(IBinder target) {
mTarget = target;
}
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
return mTarget.transact(code, data, reply, flags);
}
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/LauncherClientProxyService.kt
================================================
package app.lawnchair.lawnfeed
import android.app.Service
import android.content.Intent
import android.os.IBinder
class LauncherClientProxyService : Service() {
override fun onBind(intent: Intent): IBinder? {
return getBinder()
}
override fun onUnbind(intent: Intent?): Boolean {
binder?.onUnbind()
binder = null
stopSelf()
return super.onUnbind(intent)
}
private fun getBinder(): ProxyImpl {
if (binder == null) {
binder = ProxyImpl(applicationContext)
}
return binder!!
}
private var binder: ProxyImpl? = null
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/PermissionActivity.java
================================================
package app.lawnchair.lawnfeed;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionActivity extends Activity {
public static final int REQUEST_CODE = 1337;
private ResultReceiver resultReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Don't continue if the activity doesn't have an intent
if (getIntent() == null) {
finish();
return;
}
// Get permissions array which we want to request
resultReceiver = getIntent().getParcelableExtra("resultReceiver");
String[] permissionsArray = getIntent().getStringArrayExtra("permissions");
int requestCode = getIntent().getIntExtra("requestCode", REQUEST_CODE);
// Check if those permissions are already granted
if (PermissionResponse.hasPermissions(this, permissionsArray)) {
// Proceed like those permissions have been now granted
onComplete(requestCode, permissionsArray, new int[]{ PackageManager.PERMISSION_GRANTED });
} else {
// Otherwise request those permissions and wait for users response
ActivityCompat.requestPermissions(this, permissionsArray, requestCode);
}
}
private void onComplete(int requestCode, String[] permissions, int[] grantResult) {
Bundle bundle = new Bundle();
bundle.putStringArray("permissions", permissions);
bundle.putIntArray("grantResult", grantResult);
bundle.putInt("requestCode", requestCode);
// Send our callback to the result receiver
resultReceiver.send(requestCode, bundle);
finish();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResult) {
super.onRequestPermissionsResult(requestCode, permissions, grantResult);
onComplete(requestCode, permissions, grantResult);
}
public static void callAsync(Context context, String[] permissions, int requestCode, final PermissionResultCallback callback) {
// Proceed to the callback if permissions were already granted
if (PermissionResponse.hasPermissions(context, permissions)) {
callback.onComplete(new PermissionResponse(permissions, new int[]{PackageManager.PERMISSION_GRANTED}, requestCode));
return;
}
// Our result receiver to get the response asynchronously
ResultReceiver receiver = new ResultReceiver(new Handler(Looper.getMainLooper())) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
super.onReceiveResult(resultCode, resultData);
int[] grantResult = resultData.getIntArray("grantResult");
String[] permissions = resultData.getStringArray("permissions");
// Call the callback with the result
callback.onComplete(new PermissionResponse(permissions, grantResult, resultCode));
}
};
// Build our intent to launch
Intent intent = new Intent(context, PermissionActivity.class);
intent.putExtra("requestCode", requestCode);
intent.putExtra("permissions", permissions);
intent.putExtra("resultReceiver", receiver);
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
// Start our activity and wait
context.startActivity(intent);
}
// Contains results from requesting the permissions
public static class PermissionResponse {
private String[] permissions;
private int [] grantResult;
private int requestCode;
public PermissionResponse(String[] permissions, int[] grantResult, int requestCode) {
this.permissions = permissions;
this.grantResult = grantResult;
this.requestCode = requestCode;
}
public boolean isGranted() {
return (grantResult != null && grantResult.length > 0 && grantResult[0] == PackageManager.PERMISSION_GRANTED);
}
public String[] getPermissions() {
return permissions;
}
public int[] getGrantResult() {
return grantResult;
}
public int getRequestCode() {
return requestCode;
}
public static boolean hasPermissions(Context context, String[] permissionsArray) {
// If a permission isn't granted => return false
for (String permission : permissionsArray) {
if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED){
return false;
}
}
// Return true only when all permissions are granted
return true;
}
}
// Simple interface to handle our async callbacks
public interface PermissionResultCallback {
void onComplete(PermissionResponse response);
}
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/ProxyImpl.kt
================================================
package app.lawnchair.lawnfeed
import android.content.*
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.Process
import android.util.Log
import app.lawnchair.launcherclient.ILauncherClientProxy
import app.lawnchair.launcherclient.ILauncherClientProxyCallback
import app.lawnchair.launcherclient.LauncherClientProxyConnection
import app.lawnchair.launcherclient.WindowLayoutParams
import app.lawnchair.lawnfeed.bridge.TransactProxy
import app.lawnchair.lawnfeed.updater.Updater
import com.google.android.libraries.launcherclient.ILauncherOverlay
import com.google.android.libraries.launcherclient.ILauncherOverlayCallback
class ProxyImpl(val context: Context) : ILauncherClientProxy.Stub() {
private lateinit var proxyCallback: ILauncherClientProxyCallback
private var overlayCallbacks = OverlayCallbacks()
private var destroyed = false
private var serviceConnected: Boolean = false
private var overlay: ILauncherOverlay? = null
private var allowed = false
private val serviceConnection = OverlayServiceConnection()
private val serviceIntent = ProxyImpl.getServiceIntent(context)
private var serviceStatus: Int = 0
override fun reconnect(): Int {
enforcePermission()
try {
if (destroyed || serviceStatus != 0)
return serviceStatus
if (sApplicationConnection != null && sApplicationConnection?.packageName != serviceIntent.`package`)
context.unbindService(sApplicationConnection)
if (sApplicationConnection == null) {
sApplicationConnection = AppServiceConnection(serviceIntent.`package`)
if (!connectSafely(context, sApplicationConnection!!, Context.BIND_WAIVE_PRIORITY)) {
sApplicationConnection = null
}
}
if (sApplicationConnection != null) {
serviceStatus = LauncherClientProxyConnection.SERVICE_CONNECTING
if (!connectSafely(context, serviceConnection, Context.BIND_ADJUST_WITH_ACTIVITY)) {
serviceStatus = LauncherClientProxyConnection.SERVICE_DISCONNECTED
} else {
serviceConnected = true
}
}
if (serviceStatus == 0) {
proxyCallback.overlayStatusChanged(0)
}
} catch (e: Exception) {
Log.d(TAG, "error reconnecting", e)
}
return serviceStatus
}
private fun connectSafely(context: Context, conn: ServiceConnection, flags: Int): Boolean {
try {
return context.bindService(serviceIntent, conn, flags or Context.BIND_AUTO_CREATE)
} catch (e: SecurityException) {
Log.e("DrawerOverlayClient", "Unable to connect to overlay service")
return false
}
}
override fun closeOverlay(options: Int) {
enforcePermission()
overlay?.closeOverlay(options)
}
override fun endScroll() {
enforcePermission()
overlay?.endScroll()
}
override fun onPause() {
enforcePermission()
overlay?.onPause()
}
override fun onResume() {
enforcePermission()
overlay?.onResume()
}
override fun onScroll(progress: Float) {
enforcePermission()
overlay?.onScroll(progress)
}
override fun openOverlay(options: Int) {
enforcePermission()
overlay?.openOverlay(options)
}
override fun startScroll() {
enforcePermission()
overlay?.startScroll()
}
override fun windowAttached(attrs: WindowLayoutParams, options: Int) {
enforcePermission()
overlay?.windowAttached(attrs.layoutParams, overlayCallbacks, options)
}
override fun windowAttached2(bundle: Bundle) {
enforcePermission()
overlay?.windowAttached2(bundle, overlayCallbacks)
}
override fun setActivityState(activityState: Int) {
enforcePermission()
overlay?.setActivityState(activityState)
}
override fun windowDetached(isChangingConfigurations: Boolean) {
enforcePermission()
overlay?.windowDetached(isChangingConfigurations)
}
override fun onQsbClick(intent: Intent?) {
enforcePermission()
context.sendOrderedBroadcast(intent, null, object : BroadcastReceiver() {
@Suppress("NAME_SHADOWING")
override fun onReceive(context: Context?, intent: Intent?) {
proxyCallback.onQsbResult(resultCode)
}
}, null, 0, null, null)
}
override fun requestVoiceDetection(start: Boolean) {
enforcePermission()
overlay?.requestVoiceDetection(start)
}
override fun hasOverlayContent(): Boolean {
enforcePermission()
return overlay?.hasOverlayContent() ?: false
}
override fun startSearch(data: ByteArray?, bundle: Bundle?): Boolean {
enforcePermission()
return overlay?.startSearch(data, bundle) ?: false
}
override fun isVoiceDetectionRunning(): Boolean {
enforcePermission()
return overlay?.isVoiceDetectionRunning ?: false
}
override fun getVoiceSearchLanguage(): String {
enforcePermission()
return overlay?.voiceSearchLanguage ?: ""
}
override fun init(callback: ILauncherClientProxyCallback): Int {
allowed = callingPackage in TransactProxy.allowedPackages
enforcePermission()
proxyCallback = callback
Updater.checkUpdate(context)
ProxyImpl.getVersion(context)
return version
}
private val callingPackage get() = context.packageManager.getNameForUid(Binder.getCallingUid())
fun enforcePermission() {
if (!allowed)
throw SecurityException("$callingPackage is not allowed to call this service")
}
inner class OverlayCallbacks : ILauncherOverlayCallback.Stub() {
override fun overlayScrollChanged(progress: Float) {
if (!destroyed)
proxyCallback.overlayScrollChanged(progress)
}
override fun overlayStatusChanged(status: Int) {
if (!destroyed)
proxyCallback.overlayStatusChanged(status)
}
}
internal inner class AppServiceConnection(val packageName: String) : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
}
override fun onServiceDisconnected(name: ComponentName) {
if (name.packageName == packageName) {
sApplicationConnection = null
}
}
}
private inner class OverlayServiceConnection : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
overlay = ILauncherOverlay.Stub.asInterface(service)
serviceStatus = LauncherClientProxyConnection.SERVICE_CONNECTED
proxyCallback.onServiceConnected()
}
override fun onServiceDisconnected(name: ComponentName) {
overlay = null
serviceStatus = LauncherClientProxyConnection.SERVICE_DISCONNECTED
proxyCallback.onServiceDisconnected()
}
}
companion object {
const val TAG = "ProxyImpl"
private var sApplicationConnection: AppServiceConnection? = null
private var version = -1
internal fun getServiceIntent(context: Context): Intent {
val uri = Uri.parse("app://${context.packageName}:${Process.myUid()}").buildUpon()
.appendQueryParameter("v", Integer.toString(5))
.build()
return Intent("com.android.launcher3.WINDOW_OVERLAY")
.setPackage("com.google.android.googlequicksearchbox")
.setData(uri)
}
private fun getVersion(context: Context) {
val resolveService = context.packageManager.resolveService(getServiceIntent(context), PackageManager.GET_META_DATA)
version = resolveService?.serviceInfo?.metaData?.getInt("service.api.version", 1) ?: 1
Log.v("LauncherClient", "version: $version")
}
}
fun onUnbind() {
Log.d(TAG, "onUnbind")
destroyed = true
if (serviceConnected)
context.unbindService(serviceConnection)
if (sApplicationConnection != null)
context.unbindService(sApplicationConnection)
sApplicationConnection = null
}
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/bridge/TransactProxy.kt
================================================
package app.lawnchair.lawnfeed.bridge
import android.content.Context
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Binder
import android.os.IBinder
import android.os.Parcel
import app.lawnchair.lawnfeed.Manifest
class TransactProxy(private val target: IBinder, private val context: Context) : Binder() {
private val permissionGranted by lazy {
context.checkCallingPermission(Manifest.permission.CONNECT_SERVICE) == PERMISSION_GRANTED }
private val allowed by lazy { permissionGranted || callingPackage in allowedPackages }
private val callingPackage get() = context.packageManager.getNameForUid(getCallingUid())
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
enforcePermission()
return target.transact(code, data, reply, flags)
}
private fun enforcePermission() {
if (!allowed)
throw SecurityException("$callingPackage is not allowed to call this service")
}
companion object {
@JvmStatic
val allowedPackages = setOf(
"app.lawnchair",
"app.lawnchair.play",
"app.lawnchair.nightly",
"ch.deletescape.lawnchair.plah",
"ch.deletescape.lawnchair"
)
}
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/receivers/DownloadReceiver.java
================================================
package app.lawnchair.lawnfeed.receivers;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.widget.Toast;
import app.lawnchair.lawnfeed.R;
public abstract class DownloadReceiver extends BroadcastReceiver {
public String mFilename;
private long downloadId;
@Override
public void onReceive(Context context, Intent intent) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
String action = intent.getAction();
// We only want to check if the download has completed
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
// Check if file has been successfully downloaded
Cursor c = downloadManager.query(query);
if (c.moveToFirst()) {
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
int status = c.getInt(columnIndex);
switch (status) {
// If everything is fine, call the abstract method and proceed
case DownloadManager.STATUS_SUCCESSFUL:
Uri uri = Uri.parse(c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)));
onDownloadDone(uri);
break;
// Otherwise tell the user that the download failed
default:
Toast.makeText(context, R.string.download_file_error, Toast.LENGTH_LONG);
break;
}
context.unregisterReceiver(this);
}
// Close any open resources
c.close();
}
}
public void setDownloadId(long id) {
this.downloadId = id;
}
public void setFilename(String filename) {
this.mFilename = filename;
}
public abstract void onDownloadDone(Uri uri);
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/receivers/UpdateReceiver.java
================================================
package app.lawnchair.lawnfeed.receivers;
import android.Manifest;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import java.io.File;
import app.lawnchair.lawnfeed.PermissionActivity;
import app.lawnchair.lawnfeed.PermissionActivity.*;
import app.lawnchair.lawnfeed.R;
public class UpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
// Get our download link and setup receiver to install apk after download
final String link = intent.getStringExtra("downloadLink");
final DownloadReceiver receiver = new DownloadReceiver() {
@Override
public void onDownloadDone(Uri uri) {
// Seems like Android has changed the way to open package manager on Nougat and higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Open package installer and install downloaded apk file
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Pass downloaded file uri to our install intent
Uri content = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", new File(uri.getPath()));
install.setData(content);
context.startActivity(install);
} else {
// The old way before Nougat
Intent install = new Intent(Intent.ACTION_VIEW);
install.setDataAndType(uri, "application/vnd.android.package-archive");
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(install);
}
}
};
// Request permissions and run asynchronously
PermissionActivity.callAsync(context, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PermissionActivity.REQUEST_CODE,
new PermissionResultCallback() {
@Override
public void onComplete(PermissionResponse response) {
// Don't continue if permissing aren't granted
if (!response.isGranted()) {
Log.e("Updater", "No permissions granted!");
return;
}
String filename = intent.getStringExtra("filename");
// Check if our dir exists (theoretically it should, but you never know)
File outputDir = new File(Environment.getExternalStorageDirectory(), "Download");
if (!outputDir.exists()) {
outputDir.mkdir();
}
File file = new File(outputDir, filename);
// Call onDownloadDone if file already exists (not installed due to security settings, etc.)
if (file.exists()) {
receiver.onDownloadDone(Uri.parse(file.getAbsolutePath()));
return;
}
// Start downloading
Toast.makeText(context, R.string.downloading_toast, Toast.LENGTH_LONG).show();
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (link != null) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(link));
request.setDestinationUri(Uri.fromFile(file));
receiver.setDownloadId(downloadManager.enqueue(request));
}
// Register our download receiver
receiver.setFilename(filename);
context.getApplicationContext().registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
);
}
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/updater/Updater.java
================================================
package app.lawnchair.lawnfeed.updater;
import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import app.lawnchair.lawnfeed.receivers.UpdateReceiver;
import app.lawnchair.lawnfeed.R;
public class Updater {
public static final String VERSION_URL = "https://storage.codebucket.de/lawnchair/version.json";
public static final String DOWNLOAD_URL = "https://storage.codebucket.de/lawnchair/%1$s/Lawnfeed-%1$s.apk";
private static final String PREFERENCES_NAME = "updater";
public static final String PREFERENCES_LAST_CHECKED = "last_checked";
public static final String PREFERENCES_CACHED_UPDATE = "cached_update";
private static final long TWELVE_HOURS = 43200000;
private static final String TAG = "Updater";
public static final String CHANNEL_ID = "lawnfeed_updater";
public static void checkUpdate(final Context context) {
final SharedPreferences prefs = context.getSharedPreferences(PREFERENCES_NAME, Activity.MODE_PRIVATE);
// Create notification channel on Android O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
context.getResources().getString(R.string.lawnfeed_updates), NotificationManager.IMPORTANCE_DEFAULT);
context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
}
UpdaterTask task = new UpdaterTask(context, VERSION_URL, new UpdateListener() {
@Override
public void onSuccess(Update update) {
// Don't notify the user if he is running newer version than the latest
if (getBuildNumber(context) >= update.getBuildNumber()) {
Log.e(TAG, update.getBuildNumber() + " is lower than " + getBuildNumber(context) + "?");
return;
}
// We need our url as String for the Intent
String url = update.getDownloadUrl().toString();
// Intent for download task
Intent intentAction = new Intent(context, UpdateReceiver.class);
intentAction.putExtra("downloadLink", url);
intentAction.putExtra("filename", url.substring(url.lastIndexOf('/') + 1, url.length()));
// Build notification
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,0, intentAction, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(context.getResources().getString(R.string.update_available_title))
.setContentText(context.getResources().getString(R.string.update_available))
.setSmallIcon(R.drawable.ic_lawnchair)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setVibrate(new long[]{0, 100, 100, 100})
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(0, builder.build());
// Cache update
if (!update.isCached()) {
prefs.edit()
.putLong(PREFERENCES_LAST_CHECKED, System.currentTimeMillis())
.putString(PREFERENCES_CACHED_UPDATE, update.toString())
.apply();
}
}
@Override
public void onError(UpdateError error) {
Log.e(TAG, error.toString());
}
});
// Don't check for updates if last update check was not longer than 6 hours ago
long lastChecked = prefs.getLong(PREFERENCES_LAST_CHECKED, 0);
if (lastChecked + TWELVE_HOURS >= System.currentTimeMillis()) {
Log.i(TAG, "Last update check was earlier than 12 hours ago, using cached info");
task.onPostExecute(Update.fromString(prefs.getString(PREFERENCES_CACHED_UPDATE, "")));
return;
}
Log.i(TAG, "Checking for new updates");
// Run updater task in background
task.execute();
}
// Get current app build number from versionCode
public static int getBuildNumber(Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException ex) {}
return 0;
}
// Check if String is a valid URL
public static boolean isValidUrl(String url) {
try {
new URL(url);
return true;
} catch (MalformedURLException ignored) {}
return false;
}
// Check if device have a network connection
public static boolean isNetworkConnectivity(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null) {
return networkInfo.isConnected();
}
}
return false;
}
public static class Update {
private int buildNumber;
private URL download;
private boolean cached;
public Update(int buildNumber, URL download) {
this(buildNumber, download, false);
}
public Update(int buildNumber, URL download, boolean cached) {
this.buildNumber = buildNumber;
this.download = download;
this.cached = cached;
}
public Integer getBuildNumber() {
return buildNumber;
}
public URL getDownloadUrl() {
return download;
}
public boolean isCached() {
return cached;
}
@Override
public String toString() {
// Create a new json object
JSONObject obj = new JSONObject();
obj.put("buildNumber", buildNumber);
obj.put("download", download.toString());
// Return parsed json to string
return obj.toJSONString();
}
public static Update fromString(String json) {
try {
// Read json string
JSONObject obj = (JSONObject) new JSONParser().parse(json);
int buildNumber = ((Long) obj.get("buildNumber")).intValue();
URL download = new URL((String) obj.get("download"));
// Return cached update from json
return new Update(buildNumber, download, true);
} catch (IOException | ParseException ex) {
Log.e(TAG, "Invalid JSON object: " + json);
}
// Shouldn't be returned, but it may happen
return new Update(0, null, true);
}
}
public enum UpdateError {
// No internet connection available
NETWORK_NOT_AVAILABLE,
// URL for version info is not valid
VERSION_URL_MALFORMED,
// Version info is invalid or unreachable
VERSION_ERROR,
// Download URL for update is not valid
INVALID_DOWNLOAD_URL
}
public interface UpdateListener {
void onSuccess(Update update);
void onError(UpdateError error);
}
}
================================================
FILE: app/src/main/java/app/lawnchair/lawnfeed/updater/UpdaterTask.java
================================================
package app.lawnchair.lawnfeed.updater;
import android.content.Context;
import android.os.AsyncTask;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public class UpdaterTask extends AsyncTask<Void, Void, Updater.Update> {
private Context context;
private String update;
private Updater.UpdateListener listener;
public UpdaterTask(Context context, String update, Updater.UpdateListener listener) {
this.context = context;
this.update = update;
this.listener = listener;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
// No listener = no actions
if (listener == null) {
cancel(true);
return;
}
// Check if device is connected to the Internet
if (!Updater.isNetworkConnectivity(context)) {
listener.onError(Updater.UpdateError.NETWORK_NOT_AVAILABLE);
cancel(true);
return;
}
// Check if URL is valid
if (!Updater.isValidUrl(update)) {
listener.onError(Updater.UpdateError.VERSION_URL_MALFORMED);
cancel(true);
return;
}
}
@Override
protected Updater.Update doInBackground(Void... voids) {
// Our version.json from the storage server
JSONObject json = null;
try {
// Retrieve json object from URL
URL url = new URL(update);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
json = (JSONObject) new JSONParser().parse(new InputStreamReader(connection.getInputStream()));
} catch (IOException | ParseException ex) {
// Throw error if listener is not null
if (listener != null) {
listener.onError(Updater.UpdateError.VERSION_ERROR);
}
// Of course cancel the task
cancel(true);
return null;
}
// Don't continue if the json doesn't contain the last build number
if (!json.containsKey("travis_build_number")) {
return null;
}
int buildNumber = Integer.valueOf((String) json.get("travis_build_number"));
String version = (String) json.get("app_version");
String downloadUrl = String.format(Updater.DOWNLOAD_URL, version);
URL download = null;
try {
download = new URL(downloadUrl);
} catch (MalformedURLException ex) {
// Throw error if listener is not null
if (listener != null) {
listener.onError(Updater.UpdateError.INVALID_DOWNLOAD_URL);
}
// Of course cancel the task
cancel(true);
return null;
}
return new Updater.Update(buildNumber, download);
}
@Override
protected void onPostExecute(Updater.Update update) {
super.onPostExecute(update);
// Return fetched update to listener
if (listener != null) {
listener.onSuccess(update);
}
}
}
================================================
FILE: app/src/main/res/drawable/ic_lawnchair.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512.000000dp"
android:height="512.000000dp"
android:viewportWidth="512.000000"
android:viewportHeight="512.000000">
<path
android:fillColor="#ffffff"
android:pathData="M37.5 25.9c-7.4 2.3-14.8 10-17.1 17.7-2.2 7.5-2.1 10.5 0.6 17.8 2.1 5.6 10.5 14.1
209.4 213 114 113.9 208.6 208.1 210.3 209.3 5.8 4.2 16.5 5.5 24.5 3.1 4.8-1.4
13.2-8.7 15.3-13.1 2.1-4.7 3.3-10.8 2.8-15.3-1.1-10.4 8.1-0.8-212.3-221.2C107.4
73.6 62.6 29.3 58.6 27.3c-6.1-3.1-13.9-3.6-21.1-1.4zM108.2 360.3c-78.5 78.6-84.5
84.8-86.7 90.2-1.3 3.3-2.1 6.3-1.9 6.7 0.3 0.4 0.1 0.8-0.4 0.8 -1.1 0-0.8 3.9 0.9 10 3.4
12.4 13.8 20.1 26.9 19.9 5.2-0.1 17.7-4 83.8-26.1 42.7-14.2 82.4-27.4 88.2-29.3
20.7-6.6 97.2-32.4 97.7-32.9 0.3 -0.2-9.2-10.1-21-22l-21.6-21.5-35.8 12c-19.7
6.6-36.5 12.3-37.3 12.6-0.8 0.3 -1.9 0.7 -2.5 0.9 -0.5 0.1 -11.8 3.9-25 8.3-13.2
4.5-24.2 8.1-24.4 8.1-0.3 0 18.1-18.7
40.9-41.6l41.9-41.9c0.4-0.5-37.7-38.5-38.7-38.5-0.4 0-38.6 38-85 84.3zM450.5
297.7c-3.3 0.8 -58.5 19.2-59.6 19.9-0.6 0.3 8.7 10.3 20.6 22.1l21.5 21.6
17.3-5.8c9.5-3.2 19.1-7.1 21.5-8.7 17-11.6
14.4-38.7-4.6-47-4.4-2-12.9-3-16.7-2.1z" />
</vector>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">Lawnfeed</string>
<string name="update_available_title">Update available</string>
<string name="update_available">A new version of Lawnfeed is available to download!</string>
<string name="downloading_toast">Downloading...</string>
<string name="download_file_error">Error downloading file</string>
<string name="lawnfeed_updates">Lawnfeed Updates</string>
</resources>
================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="android:colorPrimary">@color/colorPrimary</item>
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:colorAccent">@color/colorAccent</item>
</style>
<!-- Transparent theme for PermissionActivity -->
<style name="Theme.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>
================================================
FILE: app/src/main/res/xml/file_paths.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
</paths>
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.30'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Sat Mar 27 07:42:52 CET 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle
================================================
include ':app', ':launcherclient'
gitextract_4wvmvqnb/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── .gitmodules ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── aidl/ │ │ └── amirz/ │ │ └── aidlbridge/ │ │ ├── IBridge.aidl │ │ └── IBridgeCallback.aidl │ ├── java/ │ │ ├── amirz/ │ │ │ └── aidlbridge/ │ │ │ ├── BridgeImpl.java │ │ │ ├── BridgeService.java │ │ │ └── TransactProxy.java │ │ └── app/ │ │ └── lawnchair/ │ │ └── lawnfeed/ │ │ ├── LauncherClientProxyService.kt │ │ ├── PermissionActivity.java │ │ ├── ProxyImpl.kt │ │ ├── bridge/ │ │ │ └── TransactProxy.kt │ │ ├── receivers/ │ │ │ ├── DownloadReceiver.java │ │ │ └── UpdateReceiver.java │ │ └── updater/ │ │ ├── Updater.java │ │ └── UpdaterTask.java │ └── res/ │ ├── drawable/ │ │ └── ic_lawnchair.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── xml/ │ └── file_paths.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle
SYMBOL INDEX (55 symbols across 8 files)
FILE: app/src/main/java/amirz/aidlbridge/BridgeImpl.java
class BridgeImpl (line 19) | class BridgeImpl extends IBridge.Stub {
method BridgeImpl (line 27) | BridgeImpl(Context context, Intent intent) {
method getPackage (line 40) | String getPackage() {
method bindService (line 44) | @Override
method disconnect (line 82) | void disconnect() {
FILE: app/src/main/java/amirz/aidlbridge/BridgeService.java
class BridgeService (line 12) | public class BridgeService extends Service {
method onBind (line 18) | @Override
method onUnbind (line 42) | @Override
method getLastConnection (line 54) | public static String getLastConnection() {
FILE: app/src/main/java/amirz/aidlbridge/TransactProxy.java
class TransactProxy (line 8) | class TransactProxy extends Binder {
method TransactProxy (line 11) | TransactProxy(IBinder target) {
method onTransact (line 15) | @Override
FILE: app/src/main/java/app/lawnchair/lawnfeed/PermissionActivity.java
class PermissionActivity (line 15) | public class PermissionActivity extends Activity {
method onCreate (line 20) | @Override
method onComplete (line 45) | private void onComplete(int requestCode, String[] permissions, int[] g...
method onRequestPermissionsResult (line 56) | @Override
method callAsync (line 62) | public static void callAsync(Context context, String[] permissions, in...
class PermissionResponse (line 94) | public static class PermissionResponse {
method PermissionResponse (line 99) | public PermissionResponse(String[] permissions, int[] grantResult, i...
method isGranted (line 105) | public boolean isGranted() {
method getPermissions (line 109) | public String[] getPermissions() {
method getGrantResult (line 113) | public int[] getGrantResult() {
method getRequestCode (line 117) | public int getRequestCode() {
method hasPermissions (line 121) | public static boolean hasPermissions(Context context, String[] permi...
type PermissionResultCallback (line 135) | public interface PermissionResultCallback {
method onComplete (line 136) | void onComplete(PermissionResponse response);
FILE: app/src/main/java/app/lawnchair/lawnfeed/receivers/DownloadReceiver.java
class DownloadReceiver (line 13) | public abstract class DownloadReceiver extends BroadcastReceiver {
method onReceive (line 17) | @Override
method setDownloadId (line 53) | public void setDownloadId(long id) {
method setFilename (line 57) | public void setFilename(String filename) {
method onDownloadDone (line 61) | public abstract void onDownloadDone(Uri uri);
FILE: app/src/main/java/app/lawnchair/lawnfeed/receivers/UpdateReceiver.java
class UpdateReceiver (line 23) | public class UpdateReceiver extends BroadcastReceiver {
method onReceive (line 24) | @Override
FILE: app/src/main/java/app/lawnchair/lawnfeed/updater/Updater.java
class Updater (line 30) | public class Updater {
method checkUpdate (line 47) | public static void checkUpdate(final Context context) {
method getBuildNumber (line 119) | public static int getBuildNumber(Context context) {
method isValidUrl (line 128) | public static boolean isValidUrl(String url) {
method isNetworkConnectivity (line 138) | public static boolean isNetworkConnectivity(Context context) {
class Update (line 150) | public static class Update {
method Update (line 156) | public Update(int buildNumber, URL download) {
method Update (line 160) | public Update(int buildNumber, URL download, boolean cached) {
method getBuildNumber (line 166) | public Integer getBuildNumber() {
method getDownloadUrl (line 170) | public URL getDownloadUrl() {
method isCached (line 174) | public boolean isCached() {
method toString (line 178) | @Override
method fromString (line 189) | public static Update fromString(String json) {
type UpdateError (line 207) | public enum UpdateError {
type UpdateListener (line 221) | public interface UpdateListener {
method onSuccess (line 222) | void onSuccess(Update update);
method onError (line 224) | void onError(UpdateError error);
FILE: app/src/main/java/app/lawnchair/lawnfeed/updater/UpdaterTask.java
class UpdaterTask (line 16) | public class UpdaterTask extends AsyncTask<Void, Void, Updater.Update> {
method UpdaterTask (line 21) | public UpdaterTask(Context context, String update, Updater.UpdateListe...
method onPreExecute (line 27) | @Override
method doInBackground (line 52) | @Override
method onPostExecute (line 101) | @Override
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (64K chars).
[
{
"path": ".github/workflows/main.yml",
"chars": 1791,
"preview": "name: Build signed debug APK\n\non:\n workflow_dispatch:\n push:\n branches:\n - master\n\njobs:\n build-signed-debug-"
},
{
"path": ".gitignore",
"chars": 124,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.i"
},
{
"path": ".gitmodules",
"chars": 115,
"preview": "[submodule \"launcherclient\"]\n\tpath = launcherclient\n\turl = https://github.com/LawnchairLauncher/launcherclient.git\n"
},
{
"path": "app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "app/build.gradle",
"chars": 1553,
"preview": "apply plugin: 'com.android.application'\n\nfinal def commitHash = { ->\n final def stdout = new ByteArrayOutputStream()\n"
},
{
"path": "app/proguard-rules.pro",
"chars": 942,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 2310,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package="
},
{
"path": "app/src/main/aidl/amirz/aidlbridge/IBridge.aidl",
"chars": 157,
"preview": "package amirz.aidlbridge;\n\nimport amirz.aidlbridge.IBridgeCallback;\n\ninterface IBridge {\n oneway void bindService(in "
},
{
"path": "app/src/main/aidl/amirz/aidlbridge/IBridgeCallback.aidl",
"chars": 199,
"preview": "package amirz.aidlbridge;\n\ninterface IBridgeCallback {\n oneway void onServiceConnected(in ComponentName name, in IBin"
},
{
"path": "app/src/main/java/amirz/aidlbridge/BridgeImpl.java",
"chars": 2968,
"preview": "package amirz.aidlbridge;\n\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.content."
},
{
"path": "app/src/main/java/amirz/aidlbridge/BridgeService.java",
"chars": 1781,
"preview": "package amirz.aidlbridge;\n\nimport android.app.Service;\nimport android.content.Intent;\nimport android.net.Uri;\nimport and"
},
{
"path": "app/src/main/java/amirz/aidlbridge/TransactProxy.java",
"chars": 483,
"preview": "package amirz.aidlbridge;\n\nimport android.os.Binder;\nimport android.os.IBinder;\nimport android.os.Parcel;\nimport android"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/LauncherClientProxyService.kt",
"chars": 624,
"preview": "package app.lawnchair.lawnfeed\n\nimport android.app.Service\nimport android.content.Intent\nimport android.os.IBinder\n\nclas"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/PermissionActivity.java",
"chars": 5304,
"preview": "package app.lawnchair.lawnfeed;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Int"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/ProxyImpl.kt",
"chars": 8611,
"preview": "package app.lawnchair.lawnfeed\n\nimport android.content.*\nimport android.content.pm.PackageManager\nimport android.net.Uri"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/bridge/TransactProxy.kt",
"chars": 1311,
"preview": "package app.lawnchair.lawnfeed.bridge\n\nimport android.content.Context\nimport android.content.pm.PackageManager.PERMISSIO"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/receivers/DownloadReceiver.java",
"chars": 2174,
"preview": "package app.lawnchair.lawnfeed.receivers;\n\nimport android.app.DownloadManager;\nimport android.content.BroadcastReceiver;"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/receivers/UpdateReceiver.java",
"chars": 4319,
"preview": "package app.lawnchair.lawnfeed.receivers;\n\nimport android.Manifest;\nimport android.app.DownloadManager;\nimport android.c"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/updater/Updater.java",
"chars": 8252,
"preview": "package app.lawnchair.lawnfeed.updater;\n\nimport android.app.Activity;\nimport android.app.NotificationChannel;\nimport and"
},
{
"path": "app/src/main/java/app/lawnchair/lawnfeed/updater/UpdaterTask.java",
"chars": 3303,
"preview": "package app.lawnchair.lawnfeed.updater;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\n\nimport org.json.s"
},
{
"path": "app/src/main/res/drawable/ic_lawnchair.xml",
"chars": 1259,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 266,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 266,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 208,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#3F51B5</color>\n <color name=\"color"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 430,
"preview": "<resources>\n <string name=\"app_name\">Lawnfeed</string>\n <string name=\"update_available_title\">Update available</st"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 922,
"preview": "<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppTheme\" parent=\"android:Theme.Material.Light.DarkAc"
},
{
"path": "app/src/main/res/xml/file_paths.xml",
"chars": 107,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n <external-path name=\"external_files\" path=\".\"/>\n</paths>"
},
{
"path": "build.gradle",
"chars": 555,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n e"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 230,
"preview": "#Sat Mar 27 07:42:52 CET 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "gradle.properties",
"chars": 726,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 5296,
"preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n## Gradle start up"
},
{
"path": "gradlew.bat",
"chars": 2260,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "settings.gradle",
"chars": 34,
"preview": "include ':app', ':launcherclient'\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the LawnchairLauncher/lawnfeed GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (57.5 KB), approximately 14.2k tokens, and a symbol index with 55 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.