/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.appwidget;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.DisplayMetrics;
import android.util.SparseArray;
import android.widget.RemoteViews;
import android.widget.RemoteViews.InteractionHandler;

import com.android.internal.R;
import com.android.internal.appwidget.IAppWidgetHost;
import com.android.internal.appwidget.IAppWidgetService;

import java.lang.ref.WeakReference;
import java.util.List;

/**
 * AppWidgetHost provides the interaction with the AppWidget service for apps,
 * like the home screen, that want to embed AppWidgets in their UI.
 */
public class AppWidgetHost {

    static final int HANDLE_UPDATE = 1;
    static final int HANDLE_PROVIDER_CHANGED = 2;
    static final int HANDLE_PROVIDERS_CHANGED = 3;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    static final int HANDLE_VIEW_DATA_CHANGED = 4;
    static final int HANDLE_APP_WIDGET_REMOVED = 5;

    final static Object sServiceLock = new Object();
    @UnsupportedAppUsage
    static IAppWidgetService sService;
    static boolean sServiceInitialized = false;
    private DisplayMetrics mDisplayMetrics;

    private String mContextOpPackageName;
    @UnsupportedAppUsage
    private final Handler mHandler;
    private final int mHostId;
    private final Callbacks mCallbacks;
    private final SparseArray<AppWidgetHostListener> mListeners = new SparseArray<>();
    private InteractionHandler mInteractionHandler;

    static class Callbacks extends IAppWidgetHost.Stub {
        private final WeakReference<Handler> mWeakHandler;

        public Callbacks(Handler handler) {
            mWeakHandler = new WeakReference<>(handler);
        }

        public void updateAppWidget(int appWidgetId, RemoteViews views) {
            if (isLocalBinder() && views != null) {
                views = views.clone();
            }
            Handler handler = mWeakHandler.get();
            if (handler == null) {
                return;
            }
            Message msg = handler.obtainMessage(HANDLE_UPDATE, appWidgetId, 0, views);
            msg.sendToTarget();
        }

        public void providerChanged(int appWidgetId, AppWidgetProviderInfo info) {
            if (isLocalBinder() && info != null) {
                info = info.clone();
            }
            Handler handler = mWeakHandler.get();
            if (handler == null) {
                return;
            }
            Message msg = handler.obtainMessage(HANDLE_PROVIDER_CHANGED,
                    appWidgetId, 0, info);
            msg.sendToTarget();
        }

        public void appWidgetRemoved(int appWidgetId) {
            Handler handler = mWeakHandler.get();
            if (handler == null) {
                return;
            }
            handler.obtainMessage(HANDLE_APP_WIDGET_REMOVED, appWidgetId, 0).sendToTarget();
        }

        public void providersChanged() {
            Handler handler = mWeakHandler.get();
            if (handler == null) {
                return;
            }
            handler.obtainMessage(HANDLE_PROVIDERS_CHANGED).sendToTarget();
        }

        public void viewDataChanged(int appWidgetId, int viewId) {
            Handler handler = mWeakHandler.get();
            if (handler == null) {
                return;
            }
            Message msg = handler.obtainMessage(HANDLE_VIEW_DATA_CHANGED,
                    appWidgetId, viewId);
            msg.sendToTarget();
        }

        private static boolean isLocalBinder() {
            return Process.myPid() == Binder.getCallingPid();
        }
    }

    class UpdateHandler extends Handler {
        public UpdateHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            switch (msg.what) {
                case HANDLE_UPDATE: {
                    updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj);
                    break;
                }
                case HANDLE_APP_WIDGET_REMOVED: {
                    dispatchOnAppWidgetRemoved(msg.arg1);
                    break;
                }
                case HANDLE_PROVIDER_CHANGED: {
                    onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj);
                    break;
                }
                case HANDLE_PROVIDERS_CHANGED: {
                    onProvidersChanged();
                    break;
                }
                case HANDLE_VIEW_DATA_CHANGED: {
                    viewDataChanged(msg.arg1, msg.arg2);
                    break;
                }
            }
        }
    }

    public AppWidgetHost(Context context, int hostId) {
        this(context, hostId, null, context.getMainLooper());
    }

    @Nullable
    private AppWidgetHostListener getListener(final int appWidgetId) {
        AppWidgetHostListener tempListener = null;
        synchronized (mListeners) {
            tempListener = mListeners.get(appWidgetId);
        }
        return tempListener;
    }

    /**
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public AppWidgetHost(Context context, int hostId, InteractionHandler handler, Looper looper) {
        mContextOpPackageName = context.getOpPackageName();
        mHostId = hostId;
        mInteractionHandler = handler;
        mHandler = new UpdateHandler(looper);
        mCallbacks = new Callbacks(mHandler);
        mDisplayMetrics = context.getResources().getDisplayMetrics();
        bindService(context);
    }

    private static void bindService(Context context) {
        synchronized (sServiceLock) {
            if (sServiceInitialized) {
                return;
            }
            sServiceInitialized = true;
            PackageManager packageManager = context.getPackageManager();
            if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
                    && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
                return;
            }
            IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
            sService = IAppWidgetService.Stub.asInterface(b);
        }
    }

    /**
     * Start receiving onAppWidgetChanged calls for your AppWidgets.  Call this when your activity
     * becomes visible, i.e. from onStart() in your Activity.
     */
    public void startListening() {
        if (sService == null) {
            return;
        }
        final int[] idsToUpdate;
        synchronized (mListeners) {
            int n = mListeners.size();
            idsToUpdate = new int[n];
            for (int i = 0; i < n; i++) {
                idsToUpdate[i] = mListeners.keyAt(i);
            }
        }
        List<PendingHostUpdate> updates;
        try {
            updates = sService.startListening(
                    mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList();
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }

        int N = updates.size();
        for (int i = 0; i < N; i++) {
            PendingHostUpdate update = updates.get(i);
            switch (update.type) {
                case PendingHostUpdate.TYPE_VIEWS_UPDATE:
                    updateAppWidgetView(update.appWidgetId, update.views);
                    break;
                case PendingHostUpdate.TYPE_PROVIDER_CHANGED:
                    onProviderChanged(update.appWidgetId, update.widgetInfo);
                    break;
                case PendingHostUpdate.TYPE_VIEW_DATA_CHANGED:
                    viewDataChanged(update.appWidgetId, update.viewId);
                    break;
                case PendingHostUpdate.TYPE_APP_WIDGET_REMOVED:
                    dispatchOnAppWidgetRemoved(update.appWidgetId);
                    break;
            }
        }
    }

    /**
     * Stop receiving onAppWidgetChanged calls for your AppWidgets.  Call this when your activity is
     * no longer visible, i.e. from onStop() in your Activity.
     */
    public void stopListening() {
        if (sService == null) {
            return;
        }
        try {
            sService.stopListening(mContextOpPackageName, mHostId);
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Get a appWidgetId for a host in the calling process.
     *
     * @return a appWidgetId
     */
    public int allocateAppWidgetId() {
        if (sService == null) {
            return -1;
        }
        try {
            return sService.allocateAppWidgetId(mContextOpPackageName, mHostId);
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Starts an app widget provider configure activity for result on behalf of the caller.
     * Use this method if the provider is in another profile as you are not allowed to start
     * an activity in another profile. You can optionally provide a request code that is
     * returned in {@link Activity#onActivityResult(int, int, android.content.Intent)} and
     * an options bundle to be passed to the started activity.
     * <p>
     * Note that the provided app widget has to be bound for this method to work.
     * </p>
     *
     * @param activity The activity from which to start the configure one.
     * @param appWidgetId The bound app widget whose provider's config activity to start.
     * @param requestCode Optional request code retuned with the result.
     * @param intentFlags Optional intent flags.
     *
     * @throws android.content.ActivityNotFoundException If the activity is not found.
     *
     * @see AppWidgetProviderInfo#getProfile()
     */
    public final void startAppWidgetConfigureActivityForResult(@NonNull Activity activity,
            int appWidgetId, int intentFlags, int requestCode, @Nullable Bundle options) {
        if (sService == null) {
            return;
        }
        try {
            IntentSender intentSender = sService.createAppWidgetConfigIntentSender(
                    mContextOpPackageName, appWidgetId, intentFlags);
            if (intentSender != null) {
                activity.startIntentSenderForResult(intentSender, requestCode, null, 0, 0, 0,
                        options);
            } else {
                throw new ActivityNotFoundException();
            }
        } catch (IntentSender.SendIntentException e) {
            throw new ActivityNotFoundException();
        } catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Set the visibiity of all widgets associated with this host to hidden
     *
     * @hide
     */
    public void setAppWidgetHidden() {
        if (sService == null) {
            return;
        }
        try {
            sService.setAppWidgetHidden(mContextOpPackageName, mHostId);
        } catch (RemoteException e) {
            throw new RuntimeException("System server dead?", e);
        }
    }

    /**
     * Set the host's interaction handler.
     *
     * @hide
     */
    public void setInteractionHandler(InteractionHandler interactionHandler) {
        mInteractionHandler = interactionHandler;
    }

    /**
     * Gets a list of all the appWidgetIds that are bound to the current host
     */
    public int[] getAppWidgetIds() {
        if (sService == null) {
            return new int[0];
        }
        try {
            return sService.getAppWidgetIdsForHost(mContextOpPackageName, mHostId);
        } catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Stop listening to changes for this AppWidget.
     */
    public void deleteAppWidgetId(int appWidgetId) {
        if (sService == null) {
            return;
        }
        removeListener(appWidgetId);
        try {
            sService.deleteAppWidgetId(mContextOpPackageName, appWidgetId);
        } catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Remove all records about this host from the AppWidget manager.
     * <ul>
     *   <li>Call this when initializing your database, as it might be because of a data wipe.</li>
     *   <li>Call this to have the AppWidget manager release all resources associated with your
     *   host.  Any future calls about this host will cause the records to be re-allocated.</li>
     * </ul>
     */
    public void deleteHost() {
        if (sService == null) {
            return;
        }
        try {
            sService.deleteHost(mContextOpPackageName, mHostId);
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Remove all records about all hosts for your package.
     * <ul>
     *   <li>Call this when initializing your database, as it might be because of a data wipe.</li>
     *   <li>Call this to have the AppWidget manager release all resources associated with your
     *   host.  Any future calls about this host will cause the records to be re-allocated.</li>
     * </ul>
     */
    public static void deleteAllHosts() {
        if (sService == null) {
            return;
        }
        try {
            sService.deleteAllHosts();
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

    /**
     * Create the AppWidgetHostView for the given widget.
     * The AppWidgetHost retains a pointer to the newly-created View.
     */
    public final AppWidgetHostView createView(Context context, int appWidgetId,
            AppWidgetProviderInfo appWidget) {
        if (sService == null) {
            return null;
        }
        AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
        view.setInteractionHandler(mInteractionHandler);
        view.setAppWidget(appWidgetId, appWidget);
        setListener(appWidgetId, view);

        return view;
    }

    /**
     * Called to create the AppWidgetHostView.  Override to return a custom subclass if you
     * need it.  {@more}
     */
    protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
            AppWidgetProviderInfo appWidget) {
        return new AppWidgetHostView(context, mInteractionHandler);
    }

    /**
     * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
     */
    protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) {
        AppWidgetHostListener v = getListener(appWidgetId);

        // Convert complex to dp -- we are getting the AppWidgetProviderInfo from the
        // AppWidgetService, which doesn't have our context, hence we need to do the
        // conversion here.
        appWidget.updateDimensions(mDisplayMetrics);
        if (v != null) {
            v.onUpdateProviderInfo(appWidget);
        }
    }

    /**
     * This interface specifies the actions to be performed on the app widget based on the calls
     * from the service
     *
     * @hide
     */
    public interface AppWidgetHostListener {

        /**
         * This function is called when the service want to reset the app widget provider info
         * @param appWidget The new app widget provider info
         *
         * @hide
         */
        void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo appWidget);

        /**
         * This function is called when the RemoteViews of the app widget is updated
         * @param views The new RemoteViews to be set for the app widget
         *
         * @hide
         */
        void updateAppWidget(@Nullable RemoteViews views);

        /**
         * This function is called when the view ID is changed for the app widget
         * @param viewId The new view ID to be be set for the widget
         *
         * @hide
         */
        void onViewDataChanged(int viewId);
    }

    void dispatchOnAppWidgetRemoved(int appWidgetId) {
        removeListener(appWidgetId);
        onAppWidgetRemoved(appWidgetId);
    }

    /**
     * Called when the app widget is removed for appWidgetId
     * @param appWidgetId
     */
    public void onAppWidgetRemoved(int appWidgetId) {
        // Does nothing
    }

    /**
     * Called when the set of available widgets changes (ie. widget containing packages
     * are added, updated or removed, or widget components are enabled or disabled.)
     */
    protected void onProvidersChanged() {
        // Does nothing
    }

    /**
     * Create an AppWidgetHostListener for the given widget.
     * The AppWidgetHost retains a pointer to the newly-created listener.
     * @param appWidgetId The ID of the app widget for which to add the listener
     * @param listener The listener interface that deals with actions towards the widget view
     * @hide
     */
    public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
        synchronized (mListeners) {
            mListeners.put(appWidgetId, listener);
        }
        RemoteViews views = null;
        try {
            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
        } catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
        listener.updateAppWidget(views);
    }

    /**
     * Delete the listener for the given widget
     * @param appWidgetId The ID of the app widget for which the listener is to be deleted

     * @hide
     */
    public void removeListener(int appWidgetId) {
        synchronized (mListeners) {
            mListeners.remove(appWidgetId);
        }
    }

    void updateAppWidgetView(int appWidgetId, RemoteViews views) {
        AppWidgetHostListener v = getListener(appWidgetId);
        if (v != null) {
            v.updateAppWidget(views);
        }
    }

    void viewDataChanged(int appWidgetId, int viewId) {
        AppWidgetHostListener v = getListener(appWidgetId);
        if (v != null) {
            v.onViewDataChanged(viewId);
        }
    }

    /**
     * Clear the list of Views that have been created by this AppWidgetHost.
     */
    protected void clearViews() {
        synchronized (mListeners) {
            mListeners.clear();
        }
    }
}


