diff --git a/.gitignore b/.gitignore
index 39fb081..6b1e3b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,6 @@
-*.iml
-.gradle
-/local.properties
-/.idea/workspace.xml
-/.idea/libraries
-.DS_Store
-/build
-/captures
-.externalNativeBuild
+venv/
+__pycache__/
+*.pyc
+.env
+client_secret.json
+token.json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..53ea64e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+# Raspberry Pi Photo Frame
+
+A Python application that turns a Raspberry Pi 4B into a digital photo frame, fetching photos from a specific Google Photos album.
+
+## Features
+- **Random Shuffle**: Randomly picks photos from a specified album.
+- **Interval Rotation**: Changes photos every 30 seconds (configurable).
+- **Night Mode**: Turns off the display (via `vcgencmd`) between 10 PM and 7 AM to save power.
+- **Caching**: Caches downloaded images locally to reduce network usage.
+- **PyGame**: Uses hardware-accelerated PyGame for smooth full-screen rendering.
+
+## Requirements
+- Raspberry Pi (running Raspberry Pi OS)
+- Python 3
+- Google Cloud Project with Photos Library API enabled.
+
+## Setup
+
+### 1. Google Cloud Setup
+1. Go to [Google Cloud Console](https://console.cloud.google.com/).
+2. Create a new project.
+3. Enable **Google Photos Library API**.
+4. Go to **APIs & Services > OAuth consent screen**.
+ - User Type: **External**.
+ - Add Test Users: Add your email address.
+ - Scopes: Add `.../auth/photoslibrary.readonly`.
+5. Go to **Credentials**.
+ - Create Credentials -> **OAuth Client ID**.
+ - Application Type: **Desktop App**.
+ - Download the JSON file and save it as `client_secret.json` in the project root.
+
+### 2. Installation
+1. Clone this repository.
+2. Install dependencies:
+ ```bash
+ # If you need system dependencies for pygame/pillow:
+ sudo apt-get install libsdl2-2.0-0 libsdl2-image-2.0-0 libsdl2-mixer-2.0-0 libsdl2-ttf-2.0-0 libjpeg-dev zlib1g-dev
+
+ # Install python deps
+ python3 -m venv venv
+ source venv/bin/activate
+ pip install -r requirements.txt
+ ```
+
+### 3. Configuration
+- Open `main.py` and set `ALBUM_TITLE` to the name of the album you want to display.
+- Adjust `ROTATION_INTERVAL_MS` if desired.
+- Adjust `START_HOUR` and `END_HOUR` in `scheduler.py` for night mode.
+
+### 4. First Run (Authentication)
+Run the authentication script once to generate the token:
+```bash
+python3 auth.py
+```
+Follow the instructions in the console to authorize the app. This will create `token.json`.
+
+### 5. Run the Frame
+```bash
+python3 main.py
+```
+
+### 6. Autostart on Boot (Raspberry Pi)
+Create an autostart entry:
+```bash
+mkdir -p ~/.config/autostart
+nano ~/.config/autostart/photoframe.desktop
+```
+Content:
+```ini
+[Desktop Entry]
+Type=Application
+Name=PhotoFrame
+Exec=/path/to/your/venv/bin/python3 /path/to/your/main.py
+StartupNotify=false
+Terminal=false
+```
+
+## Troubleshooting
+- **Display not turning off**: Ensure `vcgencmd` works on your system. It is standard on Raspberry Pi OS.
+- **No photos**: Check the album name match exactly in `main.py`.
+- **PyGame video driver**: If running from SSH without a desktop, you might need to set `os.environ["SDL_VIDEODRIVER"] = "dummy"` for testing, or ensure you are running in the desktop environment.
diff --git a/app/.gitignore b/app/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 435c224..0000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,59 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-apply plugin: 'kotlin-kapt'
-android {
- compileSdkVersion 26
- buildToolsVersion "$buildToolsVersion"
- defaultConfig {
- applicationId "ezjob.ghn.com.ezjob"
- minSdkVersion 15
- targetSdkVersion 26
- versionCode 1
- versionName "1.0"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-}
-
-dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
- androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
- exclude group: 'com.android.support', module: 'support-annotations'
- })
- implementation "com.android.support:appcompat-v7:$supportLibVersion"
- testImplementation 'junit:junit:4.12'
- implementation 'com.android.support.constraint:constraint-layout:1.0.2'
- implementation "com.android.support:design:$supportLibVersion"
- // kotlin
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
- compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
- compile "com.android.support:cardview-v7:$supportLibVersion"
-
- // retrofit
- compile 'com.squareup.retrofit2:retrofit:2.3.0'
- compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
- compile 'com.squareup.retrofit2:converter-gson:2.3.0'
- compile 'com.squareup.retrofit2:converter-moshi:2.3.0'
- compile 'com.squareup.moshi:moshi:1.4.0'
- compile 'com.squareup.moshi:moshi-kotlin:1.5.0'
- compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
- compile 'com.squareup.okhttp3:okhttp:3.4.1'
- // rxandroid
- compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
-
- // Android component architecture
- implementation 'android.arch.lifecycle:extensions:' + rootProject.archLifecycleVersion
-
-
- // paper parcel
- compile 'nz.bradcampbell:paperparcel:2.0.2'
- compile 'nz.bradcampbell:paperparcel-kotlin:2.0.2' // Optional
- kapt 'nz.bradcampbell:paperparcel-compiler:2.0.2'
-
-}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
deleted file mode 100644
index d2989bf..0000000
--- a/app/proguard-rules.pro
+++ /dev/null
@@ -1,25 +0,0 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in C:\Users\Dell\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
diff --git a/app/src/androidTest/java/ezjob/ghn/com/ezjob/ExampleInstrumentedTest.java b/app/src/androidTest/java/ezjob/ghn/com/ezjob/ExampleInstrumentedTest.java
deleted file mode 100644
index 30e7d27..0000000
--- a/app/src/androidTest/java/ezjob/ghn/com/ezjob/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package ezjob.ghn.com.ezjob;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() throws Exception {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("ezjob.ghn.com.ezjob", appContext.getPackageName());
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
deleted file mode 100644
index ce28efa..0000000
--- a/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/MainActivity.kt b/app/src/main/java/ezjob/ghn/com/ezjob/MainActivity.kt
deleted file mode 100644
index 7db075a..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/MainActivity.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package ezjob.ghn.com.ezjob
-
-import android.support.v7.app.AppCompatActivity
-import android.os.Bundle
-
-class MainActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- }
-}
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseAdapter.kt b/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseAdapter.kt
deleted file mode 100644
index 58354bb..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseAdapter.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package ezjob.ghn.com.ezjob.base
-
-import android.support.v7.widget.RecyclerView
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-abstract class BaseAdapter> : RecyclerView.Adapter() {
- var dataSource: List = emptyList()
- set(value) {
- field = value
- notifyDataSetChanged()
- }
-
- override fun getItemCount() = dataSource.size
-
- override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): VH {
- val inflater = LayoutInflater.from(parent?.context)
- val view = inflater.inflate(getItemViewId(),parent,false)
- return instantiateViewHolder(view)
- }
-
- abstract fun getItemViewId(): Int
- abstract fun instantiateViewHolder(view : View?) : VH
-
- override fun onBindViewHolder(holder: VH, position: Int) {
- holder.onBind(getItem(position))
- }
-
- fun getItem(position: Int) = dataSource[position]
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseLifecycleActivity.kt b/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseLifecycleActivity.kt
deleted file mode 100644
index c34bfde..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseLifecycleActivity.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package ezjob.ghn.com.ezjob.base
-
-import android.arch.lifecycle.AndroidViewModel
-import android.arch.lifecycle.LifecycleRegistry
-import android.arch.lifecycle.LifecycleRegistryOwner
-import android.arch.lifecycle.ViewModelProviders
-import android.support.v7.app.AppCompatActivity
-import ezjob.ghn.com.ezjob.utils.unsafeLazy
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-abstract class BaseLifecycleActivity : AppCompatActivity(), LifecycleRegistryOwner {
- abstract val viewModelClass: Class
- protected val viewModel: T by unsafeLazy { ViewModelProviders.of(this).get(viewModelClass) }
- private val registry = LifecycleRegistry(this)
- override fun getLifecycle(): LifecycleRegistry = registry
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseViewHolder.kt b/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseViewHolder.kt
deleted file mode 100644
index edc7630..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/base/BaseViewHolder.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package ezjob.ghn.com.ezjob.base
-
-import android.support.v7.widget.RecyclerView
-import android.view.View
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-abstract class BaseViewHolder(itemView : View?) : RecyclerView.ViewHolder(itemView){
- abstract fun onBind(item : D)
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/AdvertisingIdClient.java b/app/src/main/java/ezjob/ghn/com/ezjob/data/AdvertisingIdClient.java
deleted file mode 100644
index 2799e8d..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/AdvertisingIdClient.java
+++ /dev/null
@@ -1,134 +0,0 @@
-package ezjob.ghn.com.ezjob.data;
-
-/**
- * Created by Dell on 5/25/2017.
- */
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
-import android.os.IBinder;
-import android.os.IInterface;
-import android.os.Looper;
-import android.os.Parcel;
-import android.os.RemoteException;
-
-import java.io.IOException;
-import java.util.concurrent.LinkedBlockingQueue;
-
-public final class AdvertisingIdClient {
-
- public static AdInfo getAdvertisingIdInfo(Context context) throws Exception {
- if (Looper.myLooper() == Looper.getMainLooper())
- throw new IllegalStateException("Cannot be called from the main thread");
-
- try {
- PackageManager pm = context.getPackageManager();
- pm.getPackageInfo("com.android.vending", 0);
- } catch (Exception e) {
- throw e;
- }
-
- AdvertisingConnection connection = new AdvertisingConnection();
- Intent intent = new Intent("com.google.android.gms.ads.identifier.service.START");
- intent.setPackage("com.google.android.gms");
- if (context.bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
- try {
- AdvertisingInterface adInterface = new AdvertisingInterface(connection.getBinder());
- AdInfo adInfo = new AdInfo(adInterface.getId(), adInterface.isLimitAdTrackingEnabled(true));
- return adInfo;
- } catch (Exception exception) {
- throw exception;
- } finally {
- context.unbindService(connection);
- }
- }
- throw new IOException("Google Play connection failed");
- }
-
- public static final class AdInfo {
- private final String advertisingId;
- private final boolean limitAdTrackingEnabled;
-
- AdInfo(String advertisingId, boolean limitAdTrackingEnabled) {
- this.advertisingId = advertisingId;
- this.limitAdTrackingEnabled = limitAdTrackingEnabled;
- }
-
- public String getId() {
- return this.advertisingId;
- }
-
- public boolean isLimitAdTrackingEnabled() {
- return this.limitAdTrackingEnabled;
- }
- }
-
- private static final class AdvertisingConnection implements ServiceConnection {
- private final LinkedBlockingQueue queue = new LinkedBlockingQueue(1);
- boolean retrieved = false;
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- try {
- this.queue.put(service);
- } catch (InterruptedException localInterruptedException) {
- }
- }
-
- public void onServiceDisconnected(ComponentName name) {
- }
-
- public IBinder getBinder() throws InterruptedException {
- if (this.retrieved) throw new IllegalStateException();
- this.retrieved = true;
- return this.queue.take();
- }
- }
-
- private static final class AdvertisingInterface implements IInterface {
- private IBinder binder;
-
- public AdvertisingInterface(IBinder pBinder) {
- binder = pBinder;
- }
-
- public IBinder asBinder() {
- return binder;
- }
-
- public String getId() throws RemoteException {
- Parcel data = Parcel.obtain();
- Parcel reply = Parcel.obtain();
- String id;
- try {
- data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService");
- binder.transact(1, data, reply, 0);
- reply.readException();
- id = reply.readString();
- } finally {
- reply.recycle();
- data.recycle();
- }
- return id;
- }
-
- public boolean isLimitAdTrackingEnabled(boolean paramBoolean) throws RemoteException {
- Parcel data = Parcel.obtain();
- Parcel reply = Parcel.obtain();
- boolean limitAdTracking;
- try {
- data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService");
- data.writeInt(paramBoolean ? 1 : 0);
- binder.transact(2, data, reply, 0);
- reply.readException();
- limitAdTracking = 0 != reply.readInt();
- } finally {
- reply.recycle();
- data.recycle();
- }
- return limitAdTracking;
- }
- }
-}
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/ConnectivityInterceptor.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/ConnectivityInterceptor.kt
deleted file mode 100644
index ab1138b..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/ConnectivityInterceptor.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package ezjob.ghn.com.ezjob.data.goody
-
-import android.content.Context
-import android.util.Log
-import ezjob.ghn.com.ezjob.data.model.ErrorResponse
-import ezjob.ghn.com.ezjob.R
-import ezjob.ghn.com.ezjob.utils.Constants
-import ezjob.ghn.com.ezjob.utils.LiveNetworkMonitor
-import ezjob.ghn.com.ezjob.utils.Singleton
-import ezjob.ghn.com.ezjob.utils.unsafeLazy
-
-import java.io.IOException
-
-import okhttp3.Interceptor
-import okhttp3.Response
-import java.net.HttpURLConnection
-
-/**
- * Created by Van T Tran on 09-Aug-17.
- */
-
-
-class ConnectivityInterceptor(private val context: Context) : Interceptor {
-
- val networkMonitor: LiveNetworkMonitor by unsafeLazy { LiveNetworkMonitor(context) }
-
- @Throws(IOException::class)
- override fun intercept(chain: Interceptor.Chain): Response {
- if (!networkMonitor.isConnected) {
- throw ErrorResponse(Constants.ERR_INTERNET_DISCONNECTED, context.resources.getString(R.string.error_disconnected))
- }
- val response: Response
- try {
- response = chain.proceed(chain.request())
-
- if (response.code() !== HttpURLConnection.HTTP_OK) {
-
- throw Singleton.parseError(response.body()?.string()!!)!!
- }
- return response
- } catch (e: Exception) {
- Log.d("API Err", "${e}")
- if (e is ErrorResponse)
- throw e
- else
- throw ErrorResponse(Constants.ERR_UNKNOWN, e.message)
- }
-
- val builder = chain.request().newBuilder()
- return chain.proceed(builder.build())
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyRepository.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyRepository.kt
deleted file mode 100644
index 8f73a1c..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyRepository.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package ezjob.ghn.com.ezjob.data.goody
-
-import ezjob.ghn.com.ezjob.data.model.Session
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-class GoodyRepository(val apiService: GoodyService) {
- fun getSession(deviceId: String): io.reactivex.Single {
- return apiService.getSession(deviceId = deviceId)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyRepositoryProvider.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyRepositoryProvider.kt
deleted file mode 100644
index 8c64c4a..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyRepositoryProvider.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package ezjob.ghn.com.ezjob.data.goody
-
-import android.content.Context
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-object GoodyRepositoryProvider {
- fun provideGoodyRepository(context : Context): GoodyRepository {
- return GoodyRepository(GoodyService.create(context))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyService.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyService.kt
deleted file mode 100644
index f113064..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/goody/GoodyService.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package ezjob.ghn.com.ezjob.data.goody
-
-import android.content.Context
-import ezjob.ghn.com.ezjob.data.model.Session
-import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
-import retrofit2.Retrofit
-import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
-import retrofit2.converter.moshi.MoshiConverterFactory
-import retrofit2.http.GET
-import retrofit2.http.Query
-
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-
-interface GoodyService {
-
- @GET("checker_api/user/get_session")
- fun getSession(@Query("device_id") deviceId: String): io.reactivex.Single
-
-
- companion object Factory {
- fun create(context : Context): GoodyService {
- val interceptor = HttpLoggingInterceptor()
- interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
- val errorInterceptor = ConnectivityInterceptor(context)
- val client = OkHttpClient.Builder()
- .addInterceptor(interceptor)
- .addInterceptor(errorInterceptor)
- .build()
- val retrofit = Retrofit.Builder()
- .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
- .addConverterFactory(MoshiConverterFactory.create())
- .baseUrl("http://staging.goodyapp.vn")
- .client(client)
- .build()
- return retrofit.create(GoodyService::class.java)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/ErrorResponse.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/ErrorResponse.kt
deleted file mode 100644
index 1ecc2cb..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/ErrorResponse.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model
-
-import com.google.gson.annotations.SerializedName
-import com.squareup.moshi.Json
-import paperparcel.PaperParcel
-import paperparcel.PaperParcelable
-import java.net.ConnectException
-
-/**
- * Created by Van T Tran on 09-Aug-17.
- */
-
-@PaperParcel
-data class ErrorResponse(@SerializedName("errorCode") val error : Int,
- @SerializedName("errorMessage") override val message: String? ) : PaperParcelable, Exception() {
- companion object {
- @JvmField val CREATOR = PaperParcelSession.CREATOR
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session.kt
deleted file mode 100644
index c3b7fa3..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model
-
-import android.os.Parcelable
-import com.google.gson.annotations.SerializedName
-import paperparcel.PaperParcel
-import paperparcel.PaperParcelable
-
-@PaperParcel
-data class Session(
-
-
- @SerializedName("password")
- val password: String? = null,
-
- @SerializedName("name")
- val name: String? = null,
-
- @SerializedName("sessionToken")
- val sessionToken: String? = null,
-
- @SerializedName("id")
- val id: Int? = null,
-
- @SerializedName("deviceId")
- val deviceId: String? = null,
-
- @SerializedName("status")
- val status: Int? = null,
-
- @SerializedName("username")
- val username: String? = null
-) : PaperParcelable {
- companion object {
- @JvmField val CREATOR = PaperParcelSession.CREATOR
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session/SessionLiveData.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session/SessionLiveData.kt
deleted file mode 100644
index 112236c..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session/SessionLiveData.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model.Session
-
-import android.arch.lifecycle.MediatorLiveData
-import android.content.Context
-import android.util.Log
-import ezjob.ghn.com.ezjob.data.model.Session
-import ezjob.ghn.com.ezjob.data.goody.GoodyRepositoryProvider
-import ezjob.ghn.com.ezjob.utils.retrieveAdsInfo
-import io.reactivex.Single
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.disposables.Disposable
-import io.reactivex.schedulers.Schedulers
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-class SessionLiveData(val context: Context) : MediatorLiveData>() {
-
- private var disposal: Disposable? = null
-
- init {
- getDeviceId()
- }
-
- var deviceId: String? = null
- set(value) {
- field = value
- value?.let {
- getSession(it)
- }
- }
-
- fun getSession(deviceId: String) {
- disposal = GoodyRepositoryProvider
- .provideGoodyRepository(context)
- .getSession(deviceId)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribeOn(Schedulers.io())
- .subscribe { data, error ->
- this@SessionLiveData.value = Pair(data, error)
- }
- }
-
- override fun onInactive() {
- super.onInactive()
- if (disposal?.isDisposed?.not() ?: false) {
- disposal?.dispose()
- }
- }
-
- fun getDeviceId() {
- disposal = Single.fromCallable {
- retrieveAdsInfo(context)
- }.observeOn(AndroidSchedulers.mainThread())
- .subscribeOn(Schedulers.io())
- .subscribe(
- { deviceId ->
- Log.d("DeviceId", "${deviceId}")
- this@SessionLiveData.deviceId = deviceId
- },
- { error ->
- // do something
-
- Log.d("DeviceId", "${error}")
- this@SessionLiveData.value = Pair(null!!, error)
- }
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session/SessionViewModel.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session/SessionViewModel.kt
deleted file mode 100644
index a9fee7e..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/Session/SessionViewModel.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model.Session
-
-import android.app.Application
-import android.arch.lifecycle.AndroidViewModel
-import android.arch.lifecycle.MediatorLiveData
-import android.arch.lifecycle.MutableLiveData
-import ezjob.ghn.com.ezjob.data.model.Session
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-open class SessionViewModel(application: Application?) : AndroidViewModel(application) {
- val deviceIdLiveData = MutableLiveData()
-
- val resultLiveData = SessionLiveData(application!!.applicationContext).apply {
- this.addSource(deviceIdLiveData) {
- it?.let { this.deviceId = it }
- }
- }
- val isLoadingLiveData = MediatorLiveData().apply {
- this.addSource(resultLiveData) {
- this.value = false
- }
- }
- val throwableLiveData = MediatorLiveData().apply {
- this.addSource(resultLiveData) {
- it?.second?.let {
- this.value = it
- }
- }
- }
- val sessionLiveData = MediatorLiveData().apply {
- this.addSource(resultLiveData) {
- it?.first?.let {
- this.value = it
- }
- }
- }
-
- fun callAgain() {
- if (resultLiveData.deviceId != null) {
- resultLiveData.getSession(resultLiveData.deviceId!!)
- } else {
- resultLiveData.getDeviceId()
-
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserAdapter.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserAdapter.kt
deleted file mode 100644
index f3913e0..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserAdapter.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model.UserSample
-
-import android.view.View
-import android.widget.TextView
-import ezjob.ghn.com.ezjob.R
-import ezjob.ghn.com.ezjob.base.BaseAdapter
-import ezjob.ghn.com.ezjob.base.BaseViewHolder
-import ezjob.ghn.com.ezjob.data.sample.User
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-class UserAdapter: BaseAdapter(){
- override fun getItemViewId() = R.layout.view_item
-
- override fun instantiateViewHolder(view: View?) = UserViewHolder(view)
-
-
- class UserViewHolder(itemView : View?) : BaseViewHolder(itemView){
- val tvName by lazy { itemView?.findViewById(R.id.tvName) }
- val tvDes by lazy { itemView?.findViewById(R.id.tvDescription) }
- override fun onBind(item: User) {
- tvName?.text = item.login
- tvDes?.text = item.url
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserLiveData.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserLiveData.kt
deleted file mode 100644
index da6b373..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserLiveData.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model.UserSample
-
-import android.arch.lifecycle.MediatorLiveData
-import android.util.Log
-import ezjob.ghn.com.ezjob.data.sample.GithubRepositoryProvider
-import ezjob.ghn.com.ezjob.data.sample.Result
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.disposables.Disposable
-import io.reactivex.schedulers.Schedulers
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-class UserLiveData : MediatorLiveData>(){
-
- private var disposal : Disposable? = null
-
- var location : String? = null
- set(value) {
- value?.let {
- disposal = GithubRepositoryProvider
- .provideGithubRepository()
- .searchUsers(it,"Java")
- .observeOn(AndroidSchedulers.mainThread())
- .subscribeOn(Schedulers.io())
- .subscribe { data,error ->
- Log.d("Result", "There are ${data.items.size} users at ${location} using JAVA")
- this@UserLiveData.value = Pair(data,error)
- }
- }
- }
-
- override fun onInactive() {
- super.onInactive()
- if (disposal?.isDisposed?.not() ?:false){
- disposal?.dispose()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserViewModel.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserViewModel.kt
deleted file mode 100644
index c832261..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/model/UserSample/UserViewModel.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package ezjob.ghn.com.ezjob.data.model.UserSample
-
-import android.app.Application
-import android.arch.lifecycle.AndroidViewModel
-import android.arch.lifecycle.MediatorLiveData
-import android.arch.lifecycle.MutableLiveData
-import ezjob.ghn.com.ezjob.data.sample.Result
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-open class UserViewModel(application: Application?) : AndroidViewModel(application){
- private val locationLiveData = MutableLiveData()
-
- val resultLiveData = UserLiveData().apply {
- this.addSource(locationLiveData){
- it?.let { this.location = it }
- }
- }
- val isLoadingLiveData = MediatorLiveData().apply {
- this.addSource(resultLiveData){
- this.value = false
- }
- }
- val throwableLiveData = MediatorLiveData().apply {
- this.addSource(resultLiveData){
- it?.second?.let {
- this.value = it
- }
- }
- }
- val userLiveData = MediatorLiveData().apply {
- this.addSource(resultLiveData){
- it?.first?.let {
- this.value = it
- }
- }
- }
-
- fun setLocation(location : String ){
- locationLiveData.value = location
- isLoadingLiveData.value = true
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubApiService.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubApiService.kt
deleted file mode 100644
index ad5af5f..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubApiService.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package ezjob.ghn.com.ezjob.data.sample
-
-import retrofit2.Retrofit
-import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
-import retrofit2.converter.moshi.MoshiConverterFactory
-import retrofit2.http.GET
-import retrofit2.http.Query
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-
-interface GithubApiService{
-
- @GET("search/users")
- fun search(@Query("q") query: String,
- @Query("page") page : Int = 1,
- @Query("per_page") perPage : Int = 20) : io.reactivex.Single
-
-
-
-
- companion object Factory{
- fun create() : GithubApiService {
- val retrofit = Retrofit.Builder()
- .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
- .addConverterFactory(MoshiConverterFactory.create())
- .baseUrl("https://api.github.com")
- .build()
- return retrofit.create(GithubApiService::class.java)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubRepository.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubRepository.kt
deleted file mode 100644
index 3923621..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubRepository.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package ezjob.ghn.com.ezjob.data.sample
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-class GithubRepository(val apiService: GithubApiService) {
- fun searchUsers(location: String, language: String): io.reactivex.Single {
- return apiService.search(query = "deviceId:$location language:$language")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubRepositoryProvider.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubRepositoryProvider.kt
deleted file mode 100644
index b67b488..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/GithubRepositoryProvider.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package ezjob.ghn.com.ezjob.data.sample
-
-import ezjob.ghn.com.ezjob.data.sample.GithubApiService
-import ezjob.ghn.com.ezjob.data.sample.GithubRepository
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-object GithubRepositoryProvider {
- fun provideGithubRepository(): GithubRepository {
- return GithubRepository(GithubApiService.create())
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/User.kt b/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/User.kt
deleted file mode 100644
index 5d2587c..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/data/sample/User.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package ezjob.ghn.com.ezjob.data.sample
-
-import com.google.gson.annotations.SerializedName
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-data class User(
- @SerializedName("login") val login : String,
- @SerializedName("id") val id: Long,
- @SerializedName("url") val url: String,
- @SerializedName("html_url") val htmlUrl: String,
- @SerializedName("followers_url") val followersUrl: String,
- @SerializedName("following_url") val followingUrl: String,
- @SerializedName("starred_url") val starredUrl: String,
- @SerializedName("gists_url") val gistsUrl: String,
- @SerializedName("type") val type: String,
- @SerializedName("score") val score: Int
-)
-
-data class Result(
- val total_count: Int,
- val incomplete_results : Boolean,
- val items : List
-)
\ No newline at end of file
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/screens/search/MainActivity.kt b/app/src/main/java/ezjob/ghn/com/ezjob/screens/search/MainActivity.kt
deleted file mode 100644
index 97695a7..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/screens/search/MainActivity.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package ezjob.ghn.com.ezjob.screens.search
-
-import android.arch.lifecycle.Observer
-import android.os.Bundle
-import android.support.design.widget.Snackbar
-import ezjob.ghn.com.ezjob.R
-import ezjob.ghn.com.ezjob.base.BaseLifecycleActivity
-import ezjob.ghn.com.ezjob.data.model.UserSample.UserAdapter
-import ezjob.ghn.com.ezjob.data.model.UserSample.UserViewModel
-import ezjob.ghn.com.ezjob.data.sample.Result
-
-import kotlinx.android.synthetic.main.activity_repos.*
-
-class MainActivity : BaseLifecycleActivity() {
-
- override val viewModelClass = UserViewModel::class.java
-
-
- private val adapter = UserAdapter()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_repos)
-
- rv.setHasFixedSize(true)
- rv.adapter = adapter
-
-
- if (savedInstanceState == null)
- viewModel.setLocation("Lagos")
- observeLiveData();
- }
-
- private fun observeLiveData() {
- viewModel.isLoadingLiveData.observe(this, Observer {
- it?.let { lRefresh.isRefreshing = it }
- })
- viewModel.resultLiveData.observe(this, Observer> {
- it?.let { adapter.dataSource = it?.first?.items }
- })
- viewModel.throwableLiveData.observe(this, Observer {
- it?.let { Snackbar.make(rv, it.localizedMessage, Snackbar.LENGTH_LONG).show() }
- })
- }
-
-
-// private fun observerLiveData(){
-// viewModel.isLoadingLiveData.observe(this, Observer {
-//
-// })
-// }
-
-
-}
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/screens/welcome/WelcomeActivity.kt b/app/src/main/java/ezjob/ghn/com/ezjob/screens/welcome/WelcomeActivity.kt
deleted file mode 100644
index e2eeb65..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/screens/welcome/WelcomeActivity.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package ezjob.ghn.com.ezjob.screens.welcome
-
-import android.arch.lifecycle.Observer
-import android.os.Bundle
-import android.widget.Toast
-import ezjob.ghn.com.ezjob.R
-import ezjob.ghn.com.ezjob.base.BaseLifecycleActivity
-import ezjob.ghn.com.ezjob.data.model.ErrorResponse
-import ezjob.ghn.com.ezjob.data.model.Session
-import ezjob.ghn.com.ezjob.data.model.Session.SessionViewModel
-import ezjob.ghn.com.ezjob.utils.Constants
-import ezjob.ghn.com.ezjob.utils.DialogFactory
-
-class WelcomeActivity : BaseLifecycleActivity() {
-
- override val viewModelClass = SessionViewModel::class.java
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_welcome)
- observeLiveData()
- }
-
-
- private fun observeLiveData() {
- viewModel.isLoadingLiveData.observe(this, Observer {
- // it?.let { lRefresh.isRefreshing = it }
- })
- viewModel.resultLiveData.observe(this, Observer> {
- it?.let {
- //TODO add
- Toast.makeText(this, it.first.name, Toast.LENGTH_LONG).show()
- }
- })
- viewModel.throwableLiveData.observe(this, Observer {
- it?.let {
- if (it is ErrorResponse)
- when (it.error) {
- Constants.ERR_SESSION_NOT_VALID -> {
- DialogFactory.createSimpleOkErrorDialog(
- this,
- "",
- getString(R.string.error_session_invalid).replace("_d", viewModel.resultLiveData.deviceId!!),
- getString(R.string.dialog_try_again),
- {
- viewModel.callAgain()
- }).show()
- }
-
- }
- }
- })
- }
-
-}
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/utils/LiveNetworkMonitor.kt b/app/src/main/java/ezjob/ghn/com/ezjob/utils/LiveNetworkMonitor.kt
deleted file mode 100644
index ddc1fd3..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/utils/LiveNetworkMonitor.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package ezjob.ghn.com.ezjob.utils
-
-import android.content.Context
-import android.net.ConnectivityManager
-import ezjob.ghn.com.ezjob.utils.NetworkMonitor
-
-/**
- * Created by Dell on 5/15/2017.
- */
-
-class LiveNetworkMonitor(override val context: Context) : NetworkMonitor {
-
- override val isConnected: Boolean
- get() {
- val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
-
- val activeNetwork = cm.activeNetworkInfo
- return activeNetwork != null && activeNetwork.isConnectedOrConnecting
- }
-
-}
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/utils/NetworkMonitor.kt b/app/src/main/java/ezjob/ghn/com/ezjob/utils/NetworkMonitor.kt
deleted file mode 100644
index 2f47a45..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/utils/NetworkMonitor.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package ezjob.ghn.com.ezjob.utils
-
-import android.content.Context
-
-/**
- * Created by Dell on 5/15/2017.
- */
-
-interface NetworkMonitor {
- val isConnected: Boolean
-
- val context: Context
-}
diff --git a/app/src/main/java/ezjob/ghn/com/ezjob/utils/Utils.kt b/app/src/main/java/ezjob/ghn/com/ezjob/utils/Utils.kt
deleted file mode 100644
index f7ccdcc..0000000
--- a/app/src/main/java/ezjob/ghn/com/ezjob/utils/Utils.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package ezjob.ghn.com.ezjob.utils
-
-import android.app.Dialog
-import android.content.Context
-import android.view.View
-import android.view.Window
-import java.io.IOException
-import android.widget.TextView
-import ezjob.ghn.com.ezjob.data.model.ErrorResponse
-import com.google.gson.Gson
-import ezjob.ghn.com.ezjob.R
-import ezjob.ghn.com.ezjob.data.AdvertisingIdClient
-
-
-/**
- * Created by Van T Tran on 08-Aug-17.
- */
-
-fun unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
-
-
-fun retrieveAdsInfo(context: Context): String {
- var adInfo: AdvertisingIdClient.AdInfo? = null
-
- try {
- adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
- return adInfo!!.getId()
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- return ""
-}
-
-object Constants {
- val RESPONSE_OK = 1
- val ERR_SESSION_TIMEOUT = 440
- val ERR_INTERNET_DISCONNECTED = 441
- val ERR_UNKNOWN = 443
- val ERR_SESSION_NOT_VALID = 200
-}
-
-object Singleton {
-//// val moshi : Moshi by lazy { Moshi.Builder()
-//// .add(KotlinJsonAdapterFactory())
-//// .build() }
-// // errorAdapter = JsonAdapter<>()
-//// private val errorAdapter: JsonAdapter by lazy {
-//// moshi.adapter(ErrorResponse::class.java)
-//// }
-//
-// inline fun convertToJson(data: T): String {
-// val jsonAdapter: JsonAdapter = moshi.adapter(T::class.java)
-//
-// return jsonAdapter.toJson(data)
-// }
-//
-// fun convertFromJson(jsonStr: String, dataClass: Class): T? {
-// val jsonAdapter: JsonAdapter = moshi.adapter(dataClass)
-//
-// return jsonAdapter.fromJson(jsonStr)
-// }
-
- fun parseError(jsonStr: String): ErrorResponse? {
-// val moshi = Moshi.Builder()
-// .add(KotlinJsonAdapterFactory())
-// .build()
-// var adapter = moshi.adapter(ErrorResponse::class.java)
-// return adapter.fromJson(jsonStr)
- val gson = Gson()
- return gson.fromJson(jsonStr, ErrorResponse::class.java)
- }
-
-// fun getGson(): Gson {
-// var gson = GsonBuilder()
-// .registerTypeAdapterFactory(AutoValueGsonFactory.create())
-// .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
-// .create()
-// return gson
-// }
-
-}
-
-object DialogFactory {
- fun createSimpleOkErrorDialog(context: Context, title: String, message: String, buttonTextResId: String, callback: () -> Unit): Dialog {
- val dialog = Dialog(context)
- dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
- dialog.setContentView(R.layout.dialog_confirm_layout)
-
- dialog.findViewById(R.id.confirm_dialog_message).text = message
- val tvCancel = dialog.findViewById(R.id.dialog_confirm_negative_btn) as TextView
- tvCancel.visibility = View.GONE
- dialog.findViewById(R.id.confirm_dialog_divider).setVisibility(View.INVISIBLE)
- val tvDelete = dialog.findViewById(R.id.dialog_confirm_positive_btn) as TextView
- tvDelete.text = buttonTextResId
- tvDelete.setOnClickListener(object : View.OnClickListener {
- override fun onClick(v: View) {
- dialog.dismiss()
- callback()
- }
- })
- return dialog
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 1cd2a36..0000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 1440f5e..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_repos.xml b/app/src/main/res/layout/activity_repos.xml
deleted file mode 100644
index 3e0431a..0000000
--- a/app/src/main/res/layout/activity_repos.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml
deleted file mode 100644
index 1f0c1f3..0000000
--- a/app/src/main/res/layout/activity_welcome.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/layout/dialog_confirm_layout.xml b/app/src/main/res/layout/dialog_confirm_layout.xml
deleted file mode 100644
index cf02363..0000000
--- a/app/src/main/res/layout/dialog_confirm_layout.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_item.xml b/app/src/main/res/layout/view_item.xml
deleted file mode 100644
index 13a4dfb..0000000
--- a/app/src/main/res/layout/view_item.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 00f9eaa..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 00f9eaa..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 5507303..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
deleted file mode 100644
index 4e526c9..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 8fab6a3..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 6bc7fcd..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
deleted file mode 100644
index 2c38c71..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index 1eecc0e..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index ec87dce..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 072467e..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 05ca079..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 6f67f21..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 78a6b7a..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index 8bac0f2..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 0327e13..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 68ebe33..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index bacd3e7..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index e4c8f0c..0000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- #3F51B5
- #303F9F
- #FF4081
-
-
- #4a4a4a
- #d9d9d9
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index b15349f..0000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
- EzJob
- Không có kết nối đến dữ liệu!
- Hãy gửi mã số _d cho quản lý để kích hoạt tài khoản
- Thử lại
- OK
-
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index 5885930..0000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/test/java/ezjob/ghn/com/ezjob/ExampleUnitTest.java b/app/src/test/java/ezjob/ghn/com/ezjob/ExampleUnitTest.java
deleted file mode 100644
index f683531..0000000
--- a/app/src/test/java/ezjob/ghn/com/ezjob/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package ezjob.ghn.com.ezjob;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() throws Exception {
- assertEquals(4, 2 + 2);
- }
-}
\ No newline at end of file
diff --git a/auth.py b/auth.py
new file mode 100644
index 0000000..6cd3b6f
--- /dev/null
+++ b/auth.py
@@ -0,0 +1,61 @@
+import os
+import json
+import logging
+import pickle
+import time
+from google_auth_oauthlib.flow import InstalledAppFlow
+from google.auth.transport.requests import Request
+from google.oauth2.credentials import Credentials
+
+# Scopes required to read photos
+SCOPES = ['https://www.googleapis.com/auth/photoslibrary.readonly']
+
+TOKEN_FILE = 'token.json'
+CREDENTIALS_FILE = 'client_secret.json'
+
+def authenticate():
+ creds = None
+ # The file token.json stores the user's access and refresh tokens, and is
+ # created automatically when the authorization flow completes for the first
+ # time.
+ if os.path.exists(TOKEN_FILE):
+ try:
+ creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
+ except Exception as e:
+ logging.error(f"Error loading credentials: {e}")
+
+ # If there are no (valid) credentials available, let the user log in.
+ if not creds or not creds.valid:
+ if creds and creds.expired and creds.refresh_token:
+ try:
+ creds.refresh(Request())
+ except Exception as e:
+ logging.error(f"Error refreshing token: {e}")
+ creds = None
+
+ if not creds:
+ if not os.path.exists(CREDENTIALS_FILE):
+ logging.error(f"Credentials file '{CREDENTIALS_FILE}' not found. Please download it from Google Cloud Console.")
+ return None
+
+ flow = InstalledAppFlow.from_client_secrets_file(
+ CREDENTIALS_FILE, SCOPES)
+
+ # run_console() is deprecated/removed in newer versions.
+ # Using run_local_server() is standard.
+ # On a headless Pi, you can use SSH port forwarding: ssh -L 8080:localhost:8080 pi@ip
+ logging.info("Starting authentication flow...")
+ logging.info("If running headless, ensure you forward the port (e.g. 8080).")
+
+ # Using a fixed port helps with port forwarding
+ creds = flow.run_local_server(port=8080, open_browser=False)
+
+ # Save the credentials for the next run
+ with open(TOKEN_FILE, 'w') as token:
+ token.write(creds.to_json())
+
+ return creds
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+ authenticate()
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 0956b48..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
-
- ext.kotlin_version = '1.1.3-2'
- repositories {
- google()
- jcenter()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:3.0.0-alpha6'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-
-allprojects {
- repositories {
- google()
- jcenter()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
-ext {
- buildToolsVersion = "26.0.0"
- supportLibVersion = "26.1.0"
- archLifecycleVersion = "1.0.0-alpha5"
- archRoomVersion = "1.0.0-alpha5"
-}
\ No newline at end of file
diff --git a/cache_manager.py b/cache_manager.py
new file mode 100644
index 0000000..3954ce6
--- /dev/null
+++ b/cache_manager.py
@@ -0,0 +1,50 @@
+import os
+import requests
+import hashlib
+import logging
+
+CACHE_DIR = "image_cache"
+
+if not os.path.exists(CACHE_DIR):
+ os.makedirs(CACHE_DIR)
+
+def get_cached_path(media_item_id):
+ """Returns the path where the image should be stored."""
+ # Use a hash of the ID for filename safety, or just the ID if safe
+ safe_name = hashlib.md5(media_item_id.encode('utf-8')).hexdigest() + ".jpg"
+ return os.path.join(CACHE_DIR, safe_name)
+
+def download_image(url, media_item_id, width=1920, height=1080):
+ """
+ Downloads an image from Google Photos API URL.
+ The URL accepts parameters to resize.
+ Appends =w{width}-h{height} to request specific size.
+ """
+ target_path = get_cached_path(media_item_id)
+
+ # If already cached, just return path
+ if os.path.exists(target_path):
+ return target_path
+
+ # Construct URL with dimensions
+ download_url = f"{url}=w{width}-h{height}"
+
+ try:
+ response = requests.get(download_url, stream=True)
+ if response.status_code == 200:
+ with open(target_path, 'wb') as f:
+ for chunk in response.iter_content(1024):
+ f.write(chunk)
+ logging.info(f"Downloaded {media_item_id}")
+ return target_path
+ else:
+ logging.error(f"Failed to download image: {response.status_code}")
+ return None
+ except Exception as e:
+ logging.error(f"Error downloading image: {e}")
+ return None
+
+def clear_cache():
+ """Removes all files in cache directory."""
+ for f in os.listdir(CACHE_DIR):
+ os.remove(os.path.join(CACHE_DIR, f))
diff --git a/gradle.properties b/gradle.properties
deleted file mode 100644
index aac7c9b..0000000
--- a/gradle.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-# 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
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 13372ae..0000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index e0b3b61..0000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Sun Sep 24 16:00:51 ICT 2017
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-milestone-1-all.zip
diff --git a/gradlew b/gradlew
deleted file mode 100644
index 9d82f78..0000000
--- a/gradlew
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# 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
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
-esac
-
-# 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
-
-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" ] ; 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
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
deleted file mode 100644
index 8a0b282..0000000
--- a/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@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
-
-@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=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@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 Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_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=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-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
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..8a5c5c5
--- /dev/null
+++ b/main.py
@@ -0,0 +1,207 @@
+import os
+import sys
+import time
+import random
+import logging
+import threading
+import pygame
+
+from photos_api import PhotosService
+import cache_manager
+import scheduler
+
+# Config
+ALBUM_TITLE = "Frame"
+ROTATION_INTERVAL_MS = 30000 # 30 seconds
+CHECK_INTERVAL_MS = 60000 # 1 minute
+REFRESH_ITEMS_INTERVAL_MS = 3000000 # 50 minutes (to refresh baseUrls)
+
+# Colors
+BLACK = (0, 0, 0)
+WHITE = (255, 255, 255)
+
+class PhotoFrameApp:
+ def __init__(self):
+ # Init PyGame
+ pygame.init()
+ pygame.mouse.set_visible(False)
+
+ # Setup Screen
+ self.display_info = pygame.display.Info()
+ self.screen_width = self.display_info.current_w
+ self.screen_height = self.display_info.current_h
+
+ # Fullscreen
+ self.screen = pygame.display.set_mode((self.screen_width, self.screen_height), pygame.FULLSCREEN)
+ pygame.display.set_caption("Pi Photo Frame")
+
+ # Font
+ self.font = pygame.font.SysFont('Arial', 40)
+
+ # Logic Components
+ self.scheduler = scheduler.Scheduler()
+ self.photos_service = None
+ self.media_items = []
+ self.current_image = None
+ self.next_image_path = None
+
+ self.running = True
+ self.clock = pygame.time.Clock()
+
+ # Events
+ self.EVT_NEXT_PHOTO = pygame.USEREVENT + 1
+ self.EVT_CHECK_NIGHT = pygame.USEREVENT + 2
+ self.EVT_REFRESH_ITEMS = pygame.USEREVENT + 3
+
+ # Start Init in background
+ threading.Thread(target=self.initialize_service, daemon=True).start()
+
+ # Timers
+ pygame.time.set_timer(self.EVT_NEXT_PHOTO, ROTATION_INTERVAL_MS)
+ pygame.time.set_timer(self.EVT_CHECK_NIGHT, CHECK_INTERVAL_MS)
+ pygame.time.set_timer(self.EVT_REFRESH_ITEMS, REFRESH_ITEMS_INTERVAL_MS)
+
+ def initialize_service(self):
+ try:
+ self.show_message("Authenticating...")
+ self.photos_service = PhotosService()
+ self.fetch_media_items()
+
+ # Prepare first photo immediately
+ self.prepare_next_photo()
+
+ except Exception as e:
+ logging.error(f"Init Error: {e}")
+ self.show_message(f"Error: {e}")
+
+ def fetch_media_items(self):
+ """Fetches items from the album. Updates self.media_items thread-safely (mostly)."""
+ try:
+ logging.info("Refreshing media items list...")
+ self.show_message("Updating Library...")
+ albums = self.photos_service.list_albums()
+ target_album = next((a for a in albums if a.get('title') == ALBUM_TITLE), None)
+
+ if not target_album:
+ if albums:
+ target_album = albums[0]
+ logging.warning(f"Album '{ALBUM_TITLE}' not found. Using '{target_album.get('title')}' instead.")
+ else:
+ self.show_message("No Albums found!")
+ return
+
+ new_items = self.photos_service.get_media_items(target_album['id'])
+ # Filter images
+ new_items = [i for i in new_items if 'image' in i.get('mediaMetadata', {})]
+
+ if not new_items:
+ self.show_message("No photos in album.")
+ return
+
+ self.media_items = new_items
+ logging.info(f"Loaded {len(self.media_items)} photos.")
+ self.show_message(None) # Clear message if successful
+
+ except Exception as e:
+ logging.error(f"Error fetching items: {e}")
+
+ def show_message(self, text):
+ """Renders text to the screen."""
+ self.status_text = text
+
+ def prepare_next_photo(self):
+ """Pick random photo, download, and signal main thread."""
+ if not self.media_items:
+ return
+
+ item = random.choice(self.media_items)
+ path = cache_manager.download_image(item['baseUrl'], item['id'], self.screen_width, self.screen_height)
+
+ if path:
+ self.next_image_path = path
+ else:
+ # If download fails, it might be due to expired baseUrl (if cache miss).
+ # We can trigger a refresh if we suspect that.
+ logging.warning("Download failed. BaseUrl might be expired.")
+ # We could post an event to refresh, but let's just log for now to avoid rapid loops.
+
+ def load_image_surface(self, path):
+ try:
+ img = pygame.image.load(path)
+
+ # Scale
+ img_rect = img.get_rect()
+ img_w, img_h = img_rect.width, img_rect.height
+
+ ratio = min(self.screen_width / img_w, self.screen_height / img_h)
+ new_w = int(img_w * ratio)
+ new_h = int(img_h * ratio)
+
+ img = pygame.transform.smoothscale(img, (new_w, new_h))
+ return img
+ except Exception as e:
+ logging.error(f"Failed to load image surface: {e}")
+ return None
+
+ def run(self):
+ self.status_text = "Initializing..."
+
+ while self.running:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self.running = False
+ elif event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_ESCAPE:
+ self.running = False
+
+ elif event.type == self.EVT_CHECK_NIGHT:
+ is_night = self.scheduler.check()
+ if is_night:
+ self.current_image = None
+
+ elif event.type == self.EVT_NEXT_PHOTO:
+ if not self.scheduler.night_mode_active and self.media_items:
+ # Start fetch in thread
+ threading.Thread(target=self.prepare_next_photo, daemon=True).start()
+
+ elif event.type == self.EVT_REFRESH_ITEMS:
+ if not self.scheduler.night_mode_active:
+ threading.Thread(target=self.fetch_media_items, daemon=True).start()
+
+ # Check if a new image is ready from the thread
+ if self.next_image_path:
+ new_surf = self.load_image_surface(self.next_image_path)
+ if new_surf:
+ self.current_image = new_surf
+ self.status_text = None
+ self.next_image_path = None
+
+ # Draw
+ self.screen.fill(BLACK)
+
+ if self.scheduler.night_mode_active:
+ # Screen is "off", draw nothing (black)
+ pass
+ elif self.current_image:
+ # Center image
+ rect = self.current_image.get_rect(center=(self.screen_width//2, self.screen_height//2))
+ self.screen.blit(self.current_image, rect)
+ elif self.status_text:
+ text_surf = self.font.render(self.status_text, True, WHITE)
+ rect = text_surf.get_rect(center=(self.screen_width//2, self.screen_height//2))
+ self.screen.blit(text_surf, rect)
+
+ pygame.display.flip()
+ self.clock.tick(10) # 10 FPS is enough for a static frame
+
+ pygame.quit()
+ sys.exit()
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ # Check for display environment variable for headless run (optional)
+ if not os.environ.get('DISPLAY'):
+ os.environ['DISPLAY'] = ':0'
+
+ app = PhotoFrameApp()
+ app.run()
diff --git a/photos_api.py b/photos_api.py
new file mode 100644
index 0000000..3b603f7
--- /dev/null
+++ b/photos_api.py
@@ -0,0 +1,64 @@
+import logging
+from googleapiclient.discovery import build
+import auth
+
+class PhotosService:
+ def __init__(self):
+ self.creds = auth.authenticate()
+ if not self.creds:
+ raise Exception("Authentication failed. Check credentials.")
+ self.service = build('photoslibrary', 'v1', credentials=self.creds, static_discovery=False)
+
+ def list_albums(self, page_size=50):
+ """Lists albums in the user's account."""
+ albums = []
+ next_page_token = None
+
+ while True:
+ results = self.service.albums().list(
+ pageSize=page_size,
+ pageToken=next_page_token
+ ).execute()
+
+ items = results.get('albums', [])
+ albums.extend(items)
+
+ next_page_token = results.get('nextPageToken')
+ if not next_page_token:
+ break
+
+ return albums
+
+ def get_media_items(self, album_id):
+ """Fetches all media items from a specific album."""
+ media_items = []
+ next_page_token = None
+
+ body = {
+ 'albumId': album_id,
+ 'pageSize': 100
+ }
+
+ while True:
+ if next_page_token:
+ body['pageToken'] = next_page_token
+
+ results = self.service.mediaItems().search(body=body).execute()
+ items = results.get('mediaItems', [])
+ media_items.extend(items)
+
+ next_page_token = results.get('nextPageToken')
+ if not next_page_token:
+ break
+
+ return media_items
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+ try:
+ service = PhotosService()
+ albums = service.list_albums()
+ for album in albums:
+ print(f"ID: {album['id']}, Title: {album.get('title')}, Items: {album.get('mediaItemsCount')}")
+ except Exception as e:
+ print(e)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..8b994da
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+google-api-python-client
+google-auth-oauthlib
+google-auth-httplib2
+requests
+Pillow
+pygame
diff --git a/scheduler.py b/scheduler.py
new file mode 100644
index 0000000..e2c2c49
--- /dev/null
+++ b/scheduler.py
@@ -0,0 +1,56 @@
+import datetime
+import logging
+import subprocess
+
+# Configurable Night Mode Times (24h format)
+START_HOUR = 22 # 10 PM
+END_HOUR = 7 # 7 AM
+
+def is_night_time():
+ now = datetime.datetime.now().time()
+ if START_HOUR < END_HOUR:
+ return START_HOUR <= now.hour < END_HOUR
+ else:
+ # Crosses midnight (e.g., 22 to 7)
+ return now.hour >= START_HOUR or now.hour < END_HOUR
+
+def set_display_power(on):
+ """
+ Controls the Raspberry Pi display power using vcgencmd.
+ Note: 'vcgencmd' is specific to Raspberry Pi OS.
+ If testing on non-Pi, this will log a warning but not crash.
+ """
+ state = "1" if on else "0"
+ try:
+ # Check if vcgencmd exists
+ subprocess.run(['vcgencmd', 'display_power', state], check=True, stdout=subprocess.DEVNULL)
+ logging.info(f"Display power set to {'ON' if on else 'OFF'}")
+ except FileNotFoundError:
+ logging.warning("vcgencmd not found. Skipping display power control (not on Pi?).")
+ except subprocess.CalledProcessError as e:
+ logging.error(f"Failed to set display power: {e}")
+
+class Scheduler:
+ def __init__(self):
+ self.night_mode_active = False
+
+ def check(self):
+ """
+ Checks if the mode needs to change.
+ Returns 'active' boolean.
+ """
+ currently_night = is_night_time()
+
+ if currently_night and not self.night_mode_active:
+ # Transition to night
+ logging.info("Entering Night Mode")
+ set_display_power(False)
+ self.night_mode_active = True
+
+ elif not currently_night and self.night_mode_active:
+ # Transition to day
+ logging.info("Exiting Night Mode")
+ set_display_power(True)
+ self.night_mode_active = False
+
+ return self.night_mode_active
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index e7b4def..0000000
--- a/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'