एंड्रॉइड "केवल मूल धागा जिसने दृश्य पदानुक्रम बनाया है वह अपने विचारों को छू सकता है।"


939

मैंने एंड्रॉइड में एक साधारण म्यूजिक प्लेयर बनाया है। प्रत्येक गीत के दृश्य में एक सीकबार होता है, जिसे इस तरह से कार्यान्वित किया जाता है:

public class Song extends Activity implements OnClickListener,Runnable {
    private SeekBar progress;
    private MediaPlayer mp;

    // ...

    private ServiceConnection onService = new ServiceConnection() {
          public void onServiceConnected(ComponentName className,
            IBinder rawBinder) {
              appService = ((MPService.LocalBinder)rawBinder).getService(); // service that handles the MediaPlayer
              progress.setVisibility(SeekBar.VISIBLE);
              progress.setProgress(0);
              mp = appService.getMP();
              appService.playSong(title);
              progress.setMax(mp.getDuration());
              new Thread(Song.this).start();
          }
          public void onServiceDisconnected(ComponentName classname) {
              appService = null;
          }
    };

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.song);

        // ...

        progress = (SeekBar) findViewById(R.id.progress);

        // ...
    }

    public void run() {
    int pos = 0;
    int total = mp.getDuration();
    while (mp != null && pos<total) {
        try {
            Thread.sleep(1000);
            pos = appService.getSongPosition();
        } catch (InterruptedException e) {
            return;
        } catch (Exception e) {
            return;
        }
        progress.setProgress(pos);
    }
}

यह ठीक काम करता है। अब मुझे गाने की प्रगति के कुछ सेकंड / मिनट गिनने के लिए एक टाइमर चाहिए। इसलिए मैंने TextViewलेआउट में एक डाला , इसे findViewById()अंदर ले आया onCreate(), और इसे run()बाद में डाला progress.setProgress(pos):

String time = String.format("%d:%d",
            TimeUnit.MILLISECONDS.toMinutes(pos),
            TimeUnit.MILLISECONDS.toSeconds(pos),
            TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(
                    pos))
            );
currentTime.setText(time);  // currentTime = (TextView) findViewById(R.id.current_time);

लेकिन वह आखिरी पंक्ति मुझे अपवाद देती है:

android.view.ViewRoot $ CalledFromWrongThreadException: केवल मूल धागा जिसने दृश्य पदानुक्रम बनाया है, वह अपने विचारों को छू सकता है।

फिर भी मैं मूल रूप से वही काम कर रहा हूं जैसा कि मैं कर रहा हूं SeekBar- में दृश्य बनाना onCreate, फिर उसमें स्पर्श करना run()- और यह मुझे यह शिकायत नहीं देता है।

जवाबों:


1893

आपको पृष्ठभूमि कार्य के उस भाग को स्थानांतरित करना होगा जो मुख्य थ्रेड पर UI को अपडेट करता है। इसके लिए एक सरल कोड है:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

के लिए प्रलेखन Activity.runOnUiThread

बस पृष्ठभूमि में चल रही विधि के अंदर इसे घोंसला दें, और फिर उस कोड को कॉपी करें जो ब्लॉक के बीच में किसी भी अपडेट को लागू करता है। कोड की केवल सबसे छोटी राशि संभव को शामिल करें, अन्यथा आप पृष्ठभूमि थ्रेड के उद्देश्य को हराने के लिए शुरू करते हैं।


5
एक जादू की तरह काम किया। मेरे लिए यहाँ एकमात्र समस्या यह है कि मैं error.setText(res.toString());रन () विधि के अंदर करना चाहता था , लेकिन मैं रेस का उपयोग नहीं कर सकता था क्योंकि यह अंतिम नहीं था .. बहुत बुरा
noloman

64
इस पर एक संक्षिप्त टिप्पणी। मेरे पास एक अलग धागा था जो UI को संशोधित करने की कोशिश कर रहा था, और उपरोक्त कोड ने काम किया, लेकिन मुझे गतिविधि ऑब्जेक्ट से runOnUiThread कॉल था। मुझे कुछ ऐसा करना था myActivityObject.runOnUiThread(etc)
किर्बी

1
@ किर्बी इस संदर्भ के लिए धन्यवाद। आप बस 'MainActivity.this' कर सकते हैं और यह भी काम करना चाहिए ताकि आपको अपनी गतिविधि वर्ग का संदर्भ न रखना पड़े।
JRomero

24
मुझे यह पता लगाने में थोड़ा समय लगा कि runOnUiThread()यह गतिविधि का एक तरीका है। मैं एक खंड में अपना कोड चला रहा था। मैंने करना समाप्त कर दिया getActivity().runOnUiThread(etc)और यह काम कर गया। बहुत खुबस!;
lejonl

क्या हम उस कार्य को रोक सकते हैं जो 'runOnUiThread' पद्धति के शरीर में लिखा गया है?
करण शर्मा

143

मैंने इसे runOnUiThread( new Runnable(){ ..अंदर डालकर हल किया run():

thread = new Thread(){
        @Override
        public void run() {
            try {
                synchronized (this) {
                    wait(5000);

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            dbloadingInfo.setVisibility(View.VISIBLE);
                            bar.setVisibility(View.INVISIBLE);
                            loadingText.setVisibility(View.INVISIBLE);
                        }
                    });

                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Intent mainActivity = new Intent(getApplicationContext(),MainActivity.class);
            startActivity(mainActivity);
        };
    };  
    thread.start();

2
यह एक चट्टान है। जानकारी के लिए धन्यवाद कि, यह किसी अन्य धागे के अंदर भी इस्तेमाल किया जा सकता है।
नबीन

धन्यवाद, UI थ्रेड पर वापस जाने के लिए एक थ्रेड बनाना वास्तव में दुखद है लेकिन केवल इस समाधान ने मेरे मामले को बचाया।
पियरे माउ

2
एक महत्वपूर्ण पहलू यह है कि wait(5000);रननेबल के अंदर नहीं है, अन्यथा आपका यूआई प्रतीक्षा अवधि के दौरान फ्रीज हो जाएगा। AsyncTaskइन जैसे ऑपरेशन के लिए आपको थ्रेड के बजाय उपयोग करने पर विचार करना चाहिए ।
मार्टिन

यह स्मृति रिसाव के लिए बहुत बुरा है
राफेल लीमा

सिंक्रनाइज़ किए गए ब्लॉक से परेशान क्यों? इसके अंदर का कोड यथोचित रूप से थ्रेड सुरक्षित दिखता है (हालाँकि मैं अपने शब्दों को खाने के लिए पूरी तरह तैयार हूँ)।
डेविड

69

मेरा यह समाधान:

private void setText(final TextView text,final String value){
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            text.setText(value);
        }
    });
}

इस विधि को पृष्ठभूमि थ्रेड पर कॉल करें।


त्रुटि: (73, 67) त्रुटि: गैर-स्थैतिक विधि सेट (स्ट्रिंग) को स्थिर संदर्भ से संदर्भित नहीं किया जा सकता है

1
मेरे पास मेरी परीक्षण कक्षाओं के साथ एक ही मुद्दा है। यह मेरे लिए एक आकर्षण की तरह काम करता था। हालांकि, के runOnUiThreadसाथ बदल रहा है runTestOnUiThread। धन्यवाद
१५:४५ पर पिताजी

28

आमतौर पर, यूजर इंटरफेस को शामिल करने वाली कोई भी कार्रवाई मुख्य या यूआई थ्रेड में की जानी चाहिए, यही वह है जिसमें onCreate()इवेंट हैंडलिंग को अंजाम दिया जाता है। एक तरीका है कि यकीन है कि runOnUiThread () का उपयोग कर रहा है, दूसरे हैंडलर का उपयोग कर रहा है।

ProgressBar.setProgress() एक तंत्र है जिसके लिए यह हमेशा मुख्य थ्रेड पर निष्पादित करेगा, इसलिए इसने काम किया।

दर्द रहित थ्रेडिंग देखें ।


उस लिंक पर पीड़ारहित थ्रेडिंग लेख अब एक 404. है यहाँ एक (पुराने?) ब्लॉग पीड़ारहित थ्रेडिंग पर टुकड़ा के लिए एक लिंक है - android-developers.blogspot.com/2009/05/painless-threading.html
टोनी एडम्स

20

मैं इस स्थिति में हूं, लेकिन मुझे हैंडलर ऑब्जेक्ट के साथ एक समाधान मिला।

मेरे मामले में, मैं प्रेक्षक पैटर्न के साथ एक ProgressDialog अपडेट करना चाहता हूं । मेरा दृष्टिकोण पर्यवेक्षक को लागू करता है और अद्यतन पद्धति को ओवरराइड करता है।

इसलिए, मेरा मुख्य सूत्र दृश्य बनाता है और एक और थ्रेड अद्यतन विधि को कॉल करता है जो ProgressDialop और अद्यतन करता है ...:

केवल मूल धागा जिसने एक दृश्य पदानुक्रम बनाया, वह अपने विचारों को छू सकता है।

हैंडलर ऑब्जेक्ट के साथ समस्या को हल करना संभव है।

नीचे, मेरे कोड के विभिन्न भाग:

public class ViewExecution extends Activity implements Observer{

    static final int PROGRESS_DIALOG = 0;
    ProgressDialog progressDialog;
    int currentNumber;

    public void onCreate(Bundle savedInstanceState) {

        currentNumber = 0;
        final Button launchPolicyButton =  ((Button) this.findViewById(R.id.launchButton));
        launchPolicyButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                showDialog(PROGRESS_DIALOG);
            }
        });
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog = new ProgressDialog(this);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setMessage("Loading");
            progressDialog.setCancelable(true);
            return progressDialog;
        default:
            return null;
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog.setProgress(0);
        }

    }

    // Define the Handler that receives messages from the thread and update the progress
    final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            int current = msg.arg1;
            progressDialog.setProgress(current);
            if (current >= 100){
                removeDialog (PROGRESS_DIALOG);
            }
        }
    };

    // The method called by the observer (the second thread)
    @Override
    public void update(Observable obs, Object arg1) {

        Message msg = handler.obtainMessage();
        msg.arg1 = ++currentPluginNumber;
        handler.sendMessage(msg);
    }
}

यह स्पष्टीकरण इस पृष्ठ पर पाया जा सकता है , और आपको "एक दूसरे धागे के साथ उदाहरण प्रोग्रेसडायलॉग" पढ़ना होगा।


10

आप मुख्य UI थ्रेड को परेशान किए बिना डिलीट व्यू को हैंडलर का उपयोग कर सकते हैं। यहाँ उदाहरण कोड है

new Handler(Looper.getMainLooper()).post(new Runnable() {
                                                        @Override
                                                        public void run() {
                                                           //do stuff like remove view etc
                                                            adapter.remove(selecteditem);
                                                        }
                                                    });

7

मैं देखता हूं कि आपने @ प्रोवेंस के उत्तर को स्वीकार कर लिया है। बस मामले में, आप हैंडलर का भी उपयोग कर सकते हैं! सबसे पहले, इंट फील्ड करें।

    private static final int SHOW_LOG = 1;
    private static final int HIDE_LOG = 0;

इसके बाद, एक हैंडलर उदाहरण को एक क्षेत्र के रूप में बनाएं।

    //TODO __________[ Handler ]__________
    @SuppressLint("HandlerLeak")
    protected Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            // Put code here...

            // Set a switch statement to toggle it on or off.
            switch(msg.what)
            {
            case SHOW_LOG:
            {
                ads.setVisibility(View.VISIBLE);
                break;
            }
            case HIDE_LOG:
            {
                ads.setVisibility(View.GONE);
                break;
            }
            }
        }
    };

एक विधि बनाओ।

//TODO __________[ Callbacks ]__________
@Override
public void showHandler(boolean show)
{
    handler.sendEmptyMessage(show ? SHOW_LOG : HIDE_LOG);
}

अंत में, इसे onCreate()विधि पर रखें।

showHandler(true);

7

मेरे पास एक समान मुद्दा था, और मेरा समाधान बदसूरत है, लेकिन यह काम करता है:

void showCode() {
    hideRegisterMessage(); // Hides view 
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            showRegisterMessage(); // Shows view
        }
    }, 3000); // After 3 seconds
}

2
@ R.jzadeh यह सुनकर अच्छा लगा। उस पल के बाद से मैंने उस उत्तर को लिखा, शायद अब आप इसे बेहतर तरीके से कर सकते हैं :)
Błajej

6

के Handlerसाथ उपयोग करता हूं Looper.getMainLooper()। इसने मेरे लिए अच्छा काम किया।

    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
              // Any UI task, example
              textView.setText("your text");
        }
    };
    handler.sendEmptyMessage(1);

5

इस कोड का उपयोग करें, और runOnUiThreadकार्य करने की आवश्यकता नहीं है :

private Handler handler;
private Runnable handlerTask;

void StartTimer(){
    handler = new Handler();   
    handlerTask = new Runnable()
    {
        @Override 
        public void run() { 
            // do something  
            textView.setText("some text");
            handler.postDelayed(handlerTask, 1000);    
        }
    };
    handlerTask.run();
}

5

यह स्पष्ट रूप से एक त्रुटि फेंक रहा है। यह कहता है कि जो भी धागा एक दृश्य बनाता है, केवल वही अपने विचारों को छू सकता है। ऐसा इसलिए है क्योंकि बनाया गया दृश्य उस थ्रेड स्पेस के अंदर है। दृश्य निर्माण (GUI) UI (मुख्य) थ्रेड में होता है। तो, आप हमेशा उन तरीकों का उपयोग करने के लिए यूआई थ्रेड का उपयोग करते हैं।

यहां छवि विवरण दर्ज करें

उपरोक्त चित्र में, प्रगति चर UI थ्रेड के स्थान के अंदर है। इसलिए, केवल UI थ्रेड इस वैरिएबल को एक्सेस कर सकता है। यहाँ, आप नए थ्रेड () के माध्यम से प्रगति तक पहुँच रहे हैं, और इसीलिए आपको एक त्रुटि मिली है।


4

यह मेरे लिए तब हुआ जब मैंने उपयोग करने के बजाय एक doInBackgroundसे UI परिवर्तन के लिए कहा ।AsynctaskonPostExecute

onPostExecuteमेरी समस्या को हल करने में UI से निपटना ।


1
धन्यवाद जोनाथन यह मेरा मुद्दा भी था लेकिन मुझे यह समझने के लिए थोड़ा और पढ़ना था कि आपका यहाँ क्या मतलब है। किसी और के लिए, onPostExecuteयह भी एक विधि है, AsyncTaskलेकिन यह UI थ्रेड पर चलता है। यहाँ देखें: blog.teamtreehouse.com/all-about-android-asynctasks
ciaranodc

4

Kotlin coroutines आपके कोड को इस तरह अधिक संक्षिप्त और पठनीय बना सकती है:

MainScope().launch {
    withContext(Dispatchers.Default) {
        //TODO("Background processing...")
    }
    TODO("Update UI here!")
}

या ठीक इसके विपरीत:

GlobalScope.launch {
    //TODO("Background processing...")
    withContext(Dispatchers.Main) {
        // TODO("Update UI here!")
    }
    TODO("Continue background processing...")
}

3

मैं एक ऐसे वर्ग के साथ काम कर रहा था जिसमें संदर्भ का संदर्भ नहीं था। इसलिए मेरे लिए यह संभव नहीं था कि runOnUIThread();मैं इसका इस्तेमाल करता view.post();और इसका हल होता।

timer.scheduleAtFixedRate(new TimerTask() {

    @Override
    public void run() {
        final int currentPosition = mediaPlayer.getCurrentPosition();
        audioMessage.seekBar.setProgress(currentPosition / 1000);
        audioMessage.tvPlayDuration.post(new Runnable() {
            @Override
            public void run() {
                audioMessage.tvPlayDuration.setText(ChatDateTimeFormatter.getDuration(currentPosition));
            }
        });
    }
}, 0, 1000);

प्रश्न कोड के अनुरूप audioMessageऔर क्या है tvPlayDuration?
गत

audioMessageपाठ दृश्य का धारक ऑब्जेक्ट है। tvPlayDurationवह टेक्स्ट दृश्य है जिसे हम गैर UI थ्रेड से अपडेट करना चाहते हैं। उपरोक्त प्रश्न में, currentTimeपाठ दृश्य है लेकिन इसमें धारक वस्तु नहीं है।
इफ्ता

3

AsyncTask का उपयोग करते समय onPostExecute विधि में UI अपडेट करें

    @Override
    protected void onPostExecute(String s) {
   // Update UI here

     }

यह मेरे साथ हुआ। मैं asynk कार्य के dobackbackground में ui अद्यतन कर रहा था।
mehmoodnisar125

3

मैं इसी तरह की समस्या का सामना कर रहा था और ऊपर वर्णित तरीकों में से कोई भी मेरे लिए काम नहीं करता था। अंत में, इसने मेरे लिए चाल चली:

Device.BeginInvokeOnMainThread(() =>
    {
        myMethod();
    });

मुझे यह मणि यहां मिली ।


2

यह उल्लेखित अपवाद का ढेर है

        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6149)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:843)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
        at android.view.View.requestLayout(View.java:16474)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
        at android.view.View.setFlags(View.java:8938)
        at android.view.View.setVisibility(View.java:6066)

इसलिए अगर तुम जाओ और खोदो तो तुम्हें पता चले

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

जहां mThread नीचे की तरह कंस्ट्रक्टर में इनिशियलाइज़ होता है

mThread = Thread.currentThread();

मेरे कहने का तात्पर्य यह है कि जब हमने विशेष दृश्य बनाया तो हमने इसे UI थ्रेड पर बनाया और बाद में वर्कर थ्रेड में संशोधन करने का प्रयास किया।

हम इसे नीचे दिए गए कोड स्निपेट के माध्यम से सत्यापित कर सकते हैं

Thread.currentThread().getName()

जब हम लेआउट को फुलाते हैं और बाद में जहां आप अपवाद प्राप्त कर रहे हैं।


2

यदि आप runOnUiThreadएपीआई का उपयोग नहीं करना चाहते हैं , तो आप वास्तव में AsynTaskउन कार्यों के लिए कार्यान्वयन कर सकते हैं जिन्हें पूरा करने में कुछ सेकंड लगते हैं। लेकिन उस स्थिति में, अपने कार्य को संसाधित करने के बाद भी doinBackground(), आपको तैयार दृश्य को वापस करना होगा onPostExecute()। Android कार्यान्वयन केवल मुख्य UI थ्रेड को विचारों के साथ इंटरैक्ट करने की अनुमति देता है।


2

यदि आप बस अपने गैर UI थ्रेड से अमान्य (कॉल रिप्रेंट / रिड्रा फंक्शन) करना चाहते हैं, तो postInvalidate () का उपयोग करें

myView.postInvalidate();

यह UI-थ्रेड पर एक अमान्य अनुरोध पोस्ट करेगा।

अधिक जानकारी के लिए: क्या करता है-पोस्टिनवलिडेट-करते हैं


1

मेरे लिए मुद्दा यह था कि मैं onProgressUpdate()अपने कोड से स्पष्ट रूप से कॉल कर रहा था । यह नहीं किया जाना चाहिए। मैंने publishProgress()इसके बजाय कॉल किया और उस त्रुटि को हल किया।


1

मेरे मामले में, मेरे पास है EditText एडाप्टर है, और यह पहले से ही यूआई थ्रेड में है। हालाँकि, जब यह गतिविधि लोड होती है, तो यह इस त्रुटि के साथ क्रैश हो जाती है।

मेरा समाधान मुझे <requestFocus />XML में EditText से निकालने की आवश्यकता है ।


1

कोटलिन में संघर्ष कर रहे लोगों के लिए, यह इस तरह से काम करता है:

lateinit var runnable: Runnable //global variable

 runOnUiThread { //Lambda
            runnable = Runnable {

                //do something here

                runDelayedHandler(5000)
            }
        }

        runnable.run()

 //you need to keep the handler outside the runnable body to work in kotlin
 fun runDelayedHandler(timeToWait: Long) {

        //Keep it running
        val handler = Handler()
        handler.postDelayed(runnable, timeToWait)
    }

0

हल: बस इस विधि को doInBackround Class में डालें ... और संदेश पास करें

public void setProgressText(final String progressText){
        Handler handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // Any UI task, example
                progressDialog.setMessage(progressText);
            }
        };
        handler.sendEmptyMessage(1);

    }

0

मेरे मामले में, कॉल करने वाले को बहुत कम समय में कई बार यह त्रुटि मिल जाएगी, मैंने बस कुछ भी नहीं करने के लिए कुछ समय की जाँच करने के लिए समय बिताया, उदाहरण के लिए यदि फ़ंक्शन 0.5 सेकंड से कम कॉल किया जाता है, तो इसे अनदेखा करें:

    private long mLastClickTime = 0;

    public boolean foo() {
        if ( (SystemClock.elapsedRealtime() - mLastClickTime) < 500) {
            return false;
        }
        mLastClickTime = SystemClock.elapsedRealtime();

        //... do ui update
    }

बेहतर समाधान होगा कि क्लिक पर बटन को निष्क्रिय कर दें और कार्रवाई पूरी होने पर इसे फिर से सक्षम करें।
lsrom

@lsrom मेरे मामले में यह आसान नहीं है क्योंकि कॉलर 3 पार्टी लाइब्रेरी आंतरिक और मेरे नियंत्रण से बाहर है।
फल

0

यदि आप एक UIThread नहीं पा सकते हैं तो आप इस तरह से उपयोग कर सकते हैं।

yourcurrentcontext का मतलब है, आपको करंट कॉन्सेप्ट को पार्स करने की जरूरत है

 new Thread(new Runnable() {
        public void run() {
            while (true) {
                (Activity) yourcurrentcontext).runOnUiThread(new Runnable() {
                    public void run() { 
                        Log.d("Thread Log","I am from UI Thread");
                    }
                });
                try {
                    Thread.sleep(1000);
                } catch (Exception ex) {

                }
            }
        }
    }).start();

0

कोटलिन उत्तर

हमें सही तरीके से काम के लिए UI थ्रेड का उपयोग करना होगा। हम Kotlin में UI थ्रेड का उपयोग कर सकते हैं:

runOnUiThread(Runnable {
   //TODO: Your job is here..!
})

@canerkaseler


0

में Kotlin बस runOnUiThread गतिविधि विधि में अपने कोड डाल

runOnUiThread{
    // write your code here, for example
    val task = Runnable {
            Handler().postDelayed({
                var smzHtcList = mDb?.smzHtcReferralDao()?.getAll()
                tv_showSmzHtcList.text = smzHtcList.toString()
            }, 10)

        }
    mDbWorkerThread.postTask(task)
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.